覆盖

使用类型编程:TypeScript 中的示例

Programming with Types: Examples in TypeScript

弗拉德·里斯库蒂亚

Vlad Riscutia

版权

Copyright

有关本书和其他 Manning 书籍的在线信息和订购,请访问www.manning.com。出版商在订购大量本书时提供折扣。获取更多资讯,请联系

For online information and ordering of this and other Manning books, please visit www.manning.com. The publisher offers discounts on this book when ordered in quantity. For more information, please contact

       特约营业部
       曼宁出版公司鲍德温路 20 号
       邮政信箱 761
       纽约州庇护岛 11964
       邮箱:  orders@manning.com
       Special Sales Department
       Manning Publications Co. 20 Baldwin Road
       PO Box 761
       Shelter Island, NY 11964
       Email: orders@manning.com

©2020 Manning Publications Co. 保留所有权利。

©2020 by Manning Publications Co. All rights reserved.

未经出版商事先书面许可,不得以任何形式或通过电子、机械、影印或其他方式复制、存储在检索系统中或传播本出版物的任何部分。

No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by means electronic, mechanical, photocopying, or otherwise, without prior written permission of the publisher.

制造商和销售商用来区分其产品的许多名称都被声明为商标。如果这些名称出现在书中,并且 Manning Publications 知道商标声明,则这些名称已印在首字母大写或全部大写中。

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in the book, and Manning Publications was aware of a trademark claim, the designations have been printed in initial caps or all caps.

认识到保存所写内容的重要性,Manning 的政策是将我们出版的书籍印刷在无酸纸上,我们为此尽最大努力。还认识到我们有责任保护地球资源,Manning 书籍印刷的纸张至少有 15% 被回收利用,并且在不使用元素氯的情况下进行加工。

Recognizing the importance of preserving what has been written, it is Manning’s policy to have the books we publish printed on acid-free paper, and we exert our best efforts to that end. Recognizing also our responsibility to conserve the resources of our planet, Manning books are printed on paper that is at least 15 percent recycled and processed without the use of elemental chlorine.

曼宁出版公司鲍德温路 20 号
邮政信箱 761
纽约州庇护岛 11964
Manning Publications Co.20 Baldwin Road
PO Box 761
Shelter Island, NY 11964
开发编辑:Elesha Hyde
技术开发编辑:Mike Shepard
评论编辑:Aleksandar Dragosavljević
项目经理:Lori Weidert
文案编辑:凯西·辛普森
校对:Melody Dolab
技术校对:German Gonzalez-Morris
排版和封面设计师:Marija Tudor
Development editor: Elesha Hyde
Technical development editor: Mike Shepard
Review editor: Aleksandar Dragosavljević
Project manager: Lori Weidert
Copy editor: Kathy Simpson
Proofreader: Melody Dolab
Technical proofreader: German Gonzalez-Morris
Typesetter and cover designer: Marija Tudor

书号 9781617296413

ISBN 9781617296413

美国印制

Printed in the United States of America

奉献精神

Dedication

感谢我的妻子戴安娜,感谢她无限的耐心

To my wife, Diana, for her infinite patience

目录

Table of Contents

版权

简要目录

目录

前言

致谢

关于本书

关于封面插图

Copyright

Brief Table of Contents

Table of Contents

Preface

Acknowledgments

About This Book

About the Cover Illustration

第 1 章类型简介

1.1. 这本书是为谁而写的

1.2. 为什么存在类型

1.2.1. 0和1

1.2.2. 什么是类型和类型系统?

1.3. 类型系统的好处

1.3.1. 正确性

1.3.2. 不变性

1.3.3. 封装

1.3.4. 可组合性

1.3.5. 可读性

1.4. 类型系统的类型

1.4.1. 动态和静态类型

1.4.2. 弱类型和强类型

1.4.3. 类型推断

1.5. 在本书中

概括

第 2 章基本类型

2.1. 设计不返回值的函数

2.1.1. 空类型

2.1.2. 单位类型

2.1.3. 练习

2.2. 布尔逻辑和短路

2.2.1. 布尔表达式

2.2.2. 短路评估

2.2.3. 锻炼

2.3. 数值类型的常见陷阱

2.3.1. 整数类型和溢出

2.3.2. 浮点类型和舍入

2.3.3. 任意大的数字

2.3.4. 练习

2.4. 编码文本

2.4.1. 中断文本

2.4.2. 编码

2.4.3. 编码库

2.4.4. 练习

2.5. 使用数组和引用构建数据结构

2.5.1. 固定大小的数组

2.5.2. 参考

2.5.3. 高效列表

2.5.4. 二叉树

2.5.5. 关联数组

2.5.6. 实施权衡

2.5.7. 锻炼

概括

习题答案

设计不返回值的函数

布尔逻辑和短路

数值类型的常见陷阱

编码文本

使用数组和引用构建数据结构

第三章作文

3.1. 复合类型

3.1.1. 元组

3.1.2. 赋予意义

3.1.3. 保持不变量

3.1.4. 锻炼

3.2. 用类型表达 either-or

3.2.1. 枚举

3.2.2. 可选类型

3.2.3. 结果或错误

3.2.4. 变体

3.2.5. 练习

3.3. 访客模式

3.3.1. 天真的实现

3.3.2. 使用访问者模式

3.3.3. 访问变体

3.3.4. 锻炼

3.4. 代数数据类型

3.4.1. 产品类型

3.4.2. 求和类型

3.4.3. 练习

概括

习题答案

复合类型

用类型表达 either-or

访客模式

代数数据类型

第 4 章类型安全

4.1. 避免原始痴迷以防止误解

4.1.1. 火星气候轨道器

4.1.2. 原始的痴迷反模式

4.1.3. 锻炼

4.2. 强制约束

4.2.1. 使用构造函数强制约束

4.2.2. 对工厂实施约束

4.2.3. 锻炼

4.3. 添加类型信息

4.3.1. 类型铸造

4.3.2. 跟踪类型系统之外的类型

4.3.3. 普通类型转换

4.3.4. 练习

4.4. 隐藏和恢复类型信息

4.4.1. 异构集合

4.4.2. 连载

4.4.3. 练习

概括

习题答案

避免原始痴迷以防止误解

强制约束

添加类型信息

隐藏和恢复类型信息

第 5 章函数类型

5.1. 一个简单的策略模式

5.1.1. 功能策略

5.1.2. 类型功能

5.1.3. 战略实施

5.1.4. 一流的功能

5.1.5. 练习

5.2. 没有 switch 语句的状态机

5.2.1. 早期的类型编程

5.2.2. 状态机

5.2.3. 状态机实现回顾

5.2.4. 练习

5.3. 避免使用惰性值进行昂贵的计算

5.3.1. 拉姆达斯

5.3.2. 锻炼

5.4. 使用 map、filter 和 reduce

5.4.1. 地图()

5.4.2. 筛选()

5.4.3. 减少()

5.4.4. 库支持

5.4.5. 练习

5.5. 函数式编程

概括

习题答案

一个简单的策略模式

没有 switch 语句的状态机

避免使用惰性值进行昂贵的计算

使用 map、filter 和 reduce

第 6 章函数类型的高级应用

6.1. 一个简单的装饰器模式

6.1.1. 功能性装饰器

6.1.2. 装饰器实现

6.1.3. 闭包

6.1.4. 练习

6.2. 实现一个计数器

6.2.1. 面向对象的计数器

6.2.2. 功能计数器

6.2.3. 可恢复计数器

6.2.4. 计数器实现回顾

6.2.5. 练习

6.3. 异步执行长时间运行的操作

6.3.1. 同步执行

6.3.2. 异步执行:回调

6.3.3. 异步执行模型

6.3.4. 异步函数回顾

6.3.5. 练习

6.4. 简化异步代码

6.4.1. 链接承诺

6.4.2. 创造承诺

6.4.3. 更多关于承诺

6.4.4. 异步/等待

6.4.5. 清理异步代码回顾

6.4.6. 练习

概括

习题答案

一个简单的装饰器模式

实现一个计数器

异步执行长时间运行的操作

简化异步代码

第 7 章子类型化

7.1. 区分 TypeScript 中的相似类型

7.1.1. 结构和名义子类型的优缺点

7.1.2. 在 TypeScript 中模拟名义子类型化

7.1.3. 练习

7.2. 分配给任何东西,分配给任何东西

7.2.1. 安全反序列化

7.2.2. 错误案例的值

7.2.3. 顶部和底部类型回顾

7.2.4. 练习

7.3. 允许替换

7.3.1. 子类型和求和类型

7.3.2. 子类型和集合

7.3.3. 子类型和函数返回类型

7.3.4. 子类型和函数参数类型

7.3.5. 方差回顾

7.3.6. 练习

概括

习题答案

区分 TypeScript 中的相似类型

分配给任何东西,分配给任何东西

允许替换

第 8 章面向对象编程的要素

8.1. 使用接口定义合约

8.1.1. 练习

8.2. 继承数据和行为

8.2.1. 这是一个经验法则

8.2.2. 层次结构建模

8.2.3. 表达式的参数化行为

8.2.4. 练习

8.3. 组合数据和行为

8.3.1. 有一个经验法则

8.3.2. 组合类

8.3.3. 实施适配器模式

8.3.4. 练习

8.4. 扩展数据和行为

8.4.1. 用组合扩展行为

8.4.2. 使用混合扩展行为

8.4.3. 在 TypeScript 中混入

8.4.4. 锻炼

8.5. 纯面向对象代码的替代方案

8.5.1. 求和类型

8.5.2. 函数式编程

8.5.3. 泛型编程

概括

习题答案

使用接口定义合约

继承数据和行为

组合数据和行为

扩展数据和行为

第 9 章通用数据结构

9.1. 解耦问题

9.1.1. 可重用的身份函数

9.1.2. 可选类型

9.1.3. 通用类型

9.1.4. 练习

9.2. 通用数据布局

9.2.1. 通用数据结构

9.2.2. 什么是数据结构?

9.2.3. 练习

9.3. 遍历任何数据结构

9.3.1. 使用迭代器

9.3.2. 精简迭代代码

9.3.3. 迭代器回顾

9.3.4. 练习

9.4. 流数据

9.4.1. 加工流水线

9.4.2. 练习

概括

习题答案

解耦问题

通用数据布局

遍历任何数据结构

流数据

第 10 章通用算法和迭代器

10.1. 更好的 map()、filter()、reduce()

10.1.1。地图()

10.1.2。筛选()

10.1.3。减少()

10.1.4。过滤器()/减少()管道

10.1.5。练习

10.2. 常用算法

10.2.1。算法而不是循环

10.2.2。实施流畅的管道

10.2.3。练习

10.3. 约束类型参数

10.3.1。具有类型约束的通用数据结构

10.3.2。具有类型约束的通用算法

10.3.3。锻炼

10.4. 使用迭代器的高效逆向和其他算法

10.4.1。迭代器构建块

10.4.2。一个有用的 find()

10.4.3。高效的 reverse()

10.4.4。高效的元素检索

10.4.5。迭代器回顾

10.4.6。练习

10.5。自适应算法

10.5.1。锻炼

概括

习题答案

更好的 map()、filter()、reduce()

常用算法

约束类型参数

使用迭代器的高效逆向和其他算法

自适应算法

第 11 章高等类型及更高类型

11.1. 一张更通用的地图

11.1.1。处理结果或传播错误

11.1.2。混搭功能应用

11.1.3。函子和更高种类的类型

11.1.4。函数的函子

11.1.5。锻炼

11.2. 单子

11.2.1。结果或错误

11.2.2。map() 和 bind() 之间的区别

11.2.3。单子模式

11.2.4。延续单子

11.2.5。列表单子

11.2.6。其他单子

11.2.7。锻炼

11.3. 下一步去哪里?

11.3.1。函数式编程

11.3.2。泛型编程

11.3.3。高等类型和范畴论

11.3.4。依赖类型

11.3.5。线性类型

概括

11.4. 习题答案

一张更通用的地图

单子

A. TypeScript 安装及源​​码

在线的

当地的

源代码

DIY

B. TypeScript 备忘单

 类型和可能的值

 常用算法

地图()

筛选()

减少()

Chapter 1. Introduction to typing

1.1. Whom this book is for

1.2. Why types exist

1.2.1. 0s and 1s

1.2.2. What are types and type systems?

1.3. Benefits of type systems

1.3.1. Correctness

1.3.2. Immutability

1.3.3. Encapsulation

1.3.4. Composability

1.3.5. Readability

1.4. Types of type systems

1.4.1. Dynamic and static typing

1.4.2. Weak and strong typing

1.4.3. Type inference

1.5. In this book

Summary

Chapter 2. Basic types

2.1. Designing functions that don’t return values

2.1.1. The empty type

2.1.2. The unit type

2.1.3. Exercises

2.2. Boolean logic and short circuits

2.2.1. Boolean expressions

2.2.2. Short circuit evaluation

2.2.3. Exercise

2.3. Common pitfalls of numerical types

2.3.1. Integer types and overflow

2.3.2. Floating-point types and rounding

2.3.3. Arbitrarily large numbers

2.3.4. Exercises

2.4. Encoding text

2.4.1. Breaking text

2.4.2. Encodings

2.4.3. Encoding libraries

2.4.4. Exercises

2.5. Building data structures with arrays and references

2.5.1. Fixed-size arrays

2.5.2. References

2.5.3. Efficient lists

2.5.4. Binary trees

2.5.5. Associative arrays

2.5.6. Implementation trade-offs

2.5.7. Exercise

Summary

Answers to exercises

Designing functions that don’t return values

Boolean logic and short circuits

Common pitfalls of numerical types

Encoding text

Building data structures with arrays and references

Chapter 3. Composition

3.1. Compound types

3.1.1. Tuples

3.1.2. Assigning meaning

3.1.3. Maintaining invariants

3.1.4. Exercise

3.2. Expressing either-or with types

3.2.1. Enumerations

3.2.2. Optional types

3.2.3. Result or error

3.2.4. Variants

3.2.5. Exercises

3.3. The visitor pattern

3.3.1. A naïve implementation

3.3.2. Using the visitor pattern

3.3.3. Visiting a variant

3.3.4. Exercise

3.4. Algebraic data types

3.4.1. Product types

3.4.2. Sum types

3.4.3. Exercises

Summary

Answers to exercises

Compound types

Expressing either-or with types

The visitor pattern

Algebraic data types

Chapter 4. Type safety

4.1. Avoiding primitive obsession to prevent misinterpretation

4.1.1. The Mars Climate Orbiter

4.1.2. The primitive obsession antipattern

4.1.3. Exercise

4.2. Enforcing constraints

4.2.1. Enforcing constraints with the constructor

4.2.2. Enforcing constraints with a factory

4.2.3. Exercise

4.3. Adding type information

4.3.1. Type casting

4.3.2. Tracking types outside the type system

4.3.3. Common type casts

4.3.4. Exercises

4.4. Hiding and restoring type information

4.4.1. Heterogenous collections

4.4.2. Serialization

4.4.3. Exercises

Summary

Answers to exercises

Avoiding primitive obsession to prevent misinterpretation

Enforcing constraints

Adding type information

Hiding and restoring type information

Chapter 5. Function types

5.1. A simple strategy pattern

5.1.1. A functional strategy

5.1.2. Typing functions

5.1.3. Strategy implementations

5.1.4. First-class functions

5.1.5. Exercises

5.2. A state machine without switch statements

5.2.1. Early Programming with Types

5.2.2. State machines

5.2.3. State machine implementation recap

5.2.4. Exercises

5.3. Avoiding expensive computation with lazy values

5.3.1. Lambdas

5.3.2. Exercise

5.4. Using map, filter, and reduce

5.4.1. map()

5.4.2. filter()

5.4.3. reduce()

5.4.4. Library support

5.4.5. Exercises

5.5. Functional programming

Summary

Answers to exercises

A simple strategy pattern

A state machine without switch statements

Avoiding expensive computation with lazy values

Using map, filter, and reduce

Chapter 6. Advanced applications of function types

6.1. A simple decorator pattern

6.1.1. A functional decorator

6.1.2. Decorator implementations

6.1.3. Closures

6.1.4. Exercises

6.2. Implementing a counter

6.2.1. An object-oriented counter

6.2.2. A functional counter

6.2.3. A resumable counter

6.2.4. Counter implementations recap

6.2.5. Exercises

6.3. Executing long-running operations asynchronously

6.3.1. Synchronous execution

6.3.2. Asynchronous execution: callbacks

6.3.3. Asynchronous execution models

6.3.4. Asynchronous functions recap

6.3.5. Exercises

6.4. Simplifying asynchronous code

6.4.1. Chaining promises

6.4.2. Creating promises

6.4.3. More about promises

6.4.4. async/await

6.4.5. Clean asynchronous code recap

6.4.6. Exercises

Summary

Answers to exercises

A simple decorator pattern

Implementing a counter

Executing long-running operations asynchronously

Simplifying asynchronous code

Chapter 7. Subtyping

7.1. Distinguishing between similar types in TypeScript

7.1.1. Structural and nominal subtyping pros and cons

7.1.2. Simulating nominal subtyping in TypeScript

7.1.3. Exercises

7.2. Assigning anything to, assigning to anything

7.2.1. Safe deserialization

7.2.2. Values for error cases

7.2.3. Top and bottom types recap

7.2.4. Exercises

7.3. Allowed substitutions

7.3.1. Subtyping and sum types

7.3.2. Subtyping and collections

7.3.3. Subtyping and function return types

7.3.4. Subtyping and function argument types

7.3.5. Variance recap

7.3.6. Exercises

Summary

Answers to exercises

Distinguishing between similar types in TypeScript

Assigning anything to, assigning to anything

Allowed substitutions

Chapter 8. Elements of object-oriented programming

8.1. Defining contracts with interfaces

8.1.1. Exercises

8.2. Inheriting data and behavior

8.2.1. The is-a rule of thumb

8.2.2. Modeling a hierarchy

8.2.3. Parameterizing behavior of expressions

8.2.4. Exercises

8.3. Composing data and behavior

8.3.1. The has-a rule of thumb

8.3.2. Composite classes

8.3.3. Implementing the adapter pattern

8.3.4. Exercises

8.4. Extending data and behavior

8.4.1. Extending behavior with composition

8.4.2. Extending behavior with mix-ins

8.4.3. Mix-in in TypeScript

8.4.4. Exercise

8.5. Alternatives to purely object-oriented code

8.5.1. Sum types

8.5.2. Functional programming

8.5.3. Generic programming

Summary

Answers to exercises

Defining contracts with interfaces

Inheriting data and behavior

Composing data and behavior

Extending data and behavior

Chapter 9. Generic data structures

9.1. Decoupling concerns

9.1.1. A reusable identity function

9.1.2. The optional type

9.1.3. Generic types

9.1.4. Exercises

9.2. Generic data layout

9.2.1. Generic data structures

9.2.2. What is a data structure?

9.2.3. Exercises

9.3. Traversing any data structure

9.3.1. Using iterators

9.3.2. Streamlining iteration code

9.3.3. Iterators recap

9.3.4. Exercises

9.4. Streaming data

9.4.1. Processing pipelines

9.4.2. Exercises

Summary

Answers to exercises

Decoupling concerns

Generic data layout

Traversing any data structure

Streaming data

Chapter 10. Generic algorithms and iterators

10.1. Better map(), filter(), reduce()

10.1.1. map()

10.1.2. filter()

10.1.3. reduce()

10.1.4. filter()/reduce() pipeline

10.1.5. Exercises

10.2. Common algorithms

10.2.1. Algorithms instead of loops

10.2.2. Implementing a fluent pipeline

10.2.3. Exercises

10.3. Constraining type parameters

10.3.1. Generic data structures with type constraints

10.3.2. Generic algorithms with type constraints

10.3.3. Exercise

10.4. Efficient reverse and other algorithms using iterators

10.4.1. Iterator building blocks

10.4.2. A useful find()

10.4.3. An efficient reverse()

10.4.4. Efficient element retrieval

10.4.5. Iterator recap

10.4.6. Exercises

10.5. Adaptive algorithms

10.5.1. Exercise

Summary

Answers to exercises

Better map(), filter(), reduce()

Common algorithms

Constraining type parameters

Efficient reverse and other algorithms using iterators

Adaptive algorithms

Chapter 11. Higher kinded types and beyond

11.1. An even more general map

11.1.1. Processing results or propagating errors

11.1.2. Mix-and-match function application

11.1.3. Functors and higher kinded types

11.1.4. Functors for functions

11.1.5. Exercise

11.2. Monads

11.2.1. Result or error

11.2.2. Difference between map() and bind()

11.2.3. The monad pattern

11.2.4. The continuation monad

11.2.5. The list monad

11.2.6. Other monads

11.2.7. Exercise

11.3. Where to next?

11.3.1. Functional programming

11.3.2. Generic programming

11.3.3. Higher kinded types and category theory

11.3.4. Dependent types

11.3.5. Linear types

Summary

11.4. Answers to exercises

An even more general map

Monads

A. TypeScript installation and source code

Online

Local

Source Code

DIY

B. TypeScript cheat sheet

 Types and possible values

 Common algorithms

map()

filter()

reduce()

指数

Index

图列表

List of Figures

表格列表

List of Tables

房源清单

List of Listings

前言

Preface

Programming with Types是多年学习类型系统和软件正确性的结晶,提炼成一本具有实际应用程序的实用书籍。

Programming with Types is the culmination of multiple years of learning about type systems and software correctness, distilled into a practical book with real-world applications.

我一直喜欢学习如何编写更好的代码,但如果我要准确指出我是何时开始走这条路的,我会说是 2015 年。那时我正在更换团队并想加快速度现代 C++。我开始观看 C++ 会议视频,拿起 Alexander Stepanov 关于泛型编程的书籍,对如何编写代码有了完全不同的看法。

I’ve always liked learning how to write better code, but if I were to point out exactly when I started down this path, I’d say it was 2015. I was switching teams at that point and wanted to get up to speed on modern C++. I started watching C++ conference videos, picked up Alexander Stepanov’s books on generic programming, and gained a completely different perspective on how to write code.

同时,我在业余时间学习 Haskell,并逐步了解其类型系统的高级功能。使用函数式语言进行编程可以清楚地看到,随着时间的推移,这些语言中理所当然的一些特性如何被更多的主流语言所采用。

In parallel, I was learning Haskell in my spare time and working my way through the advanced features of its type system. Programming in a functional language makes it obvious how some of the features taken for granted in such languages get adopted by more mainstream languages as time goes by.

我读了几本关于这个主题的书,从 Stepanov 的《编程基础》和《从数学到泛型编程》到 Bartosz Milewski 的《程序员范畴论》和 Benjamin Pierce 的《类型和编程语言》。正如您可能从标题中看出的那样,这些书更多的是在理论/数学方面。在更多地了解类型系统的同时,我可以看出我在工作中编写的代码变得更好了。类型系统设计的更多理论领域与日常生产软件之间存在直接联系。这不是一个革命性的发现:奇特的类型系统特性是为了解决现实世界的问题而存在的。

I read several books on the topic, from Stepanov’s Elements of Programming and From Mathematics to Generic Programming to Bartosz Milewski’s Category Theory for Programmers and Benjamin Pierce’s Types and Programming Languages. As you might be able to tell from the titles, these books are more on the theoretical/mathematical side. While learning more about type systems, I could tell that the code I was writing at work became better. There is a direct link between the more theoretical realm of type system design and the day-to-day production software. This isn’t a revolutionary discovery: fancy type system features exist to address real-world problems.

我意识到并不是每个实践中的程序员都有时间和耐心阅读带有数学证明的厚书。另一方面,我的时间并没有浪费在读这些书上:它们让我成为了更好的软件工程师。我认为有足够的空间来介绍类型系统及其提供的更非正式的好处,重点关注任何人在日常工作中都可以使用的实际应用程序。

I realized that not every practicing programmer has the time and patience to read dense books with mathematical proofs. On the other hand, my time wasn’t wasted reading such books: they made me a better software engineer. I figured there is room for a book that covers type systems and the benefits they provide more informally, focusing on practical applications anyone can use in their day job.

Programming with Types旨在提供从基本类型开始的类型系统特性的演练,涵盖函数类型和子类型、OOP、泛型编程以及更高种类的类型,如仿函数和单子。我没有关注这些特性背后的理论,而是根据实际应用来描述它们中的每一个。本书展示了如何以及何时使用这些功能中的每一个来改进您的代码。

Programming with Types aims to provide a walk-through of type system features starting from basic types, covering function types and subtyping, OOP, generic programming, and higher kinded types such as functors and monads. Instead of focusing on the theory behind these features, I describe each one of them in terms of practical applications. The book shows how and when to use each of these features to improve your code.

代码示例最初应该使用 C++。C++ 类型系统比 Java 和 C# 等语言功能强大且功能更丰富。另一方面,C++ 是一门复杂的语言,我不想限制本书的读者,所以我决定改用 TypeScript。TypeScript 也有一个强大的类型系统,但它的语法更容易理解,所以即使你来自另一种语言,它也应该很容易理解大多数例子。附录 B 提供了本书中使用的 TypeScript 子集的快速备忘单。

The code samples were originally supposed to be in C++. The C++ type system is powerful and more feature-rich than languages such as Java and C#. On the other hand, C++ is a complex language, and I didn’t want to limit the audience of the book, so I decided to use TypeScript instead. TypeScript has a powerful type system too, but its syntax is more accessible, so it should be easy to work through most examples even if you’re coming from another language. Appendix B provides a quick cheat sheet for the subset of TypeScript used in this book.

我希望您喜欢阅读本书并学习一些可以立即应用于您的项目的新技术。

I hope you enjoy reading this book and learn some new techniques that you can apply to your projects right away.

致谢

Acknowledgments

首先,我要感谢我的家人对我的支持和理解。我的妻子戴安娜和女儿艾达一路陪伴着我,给了我完成本书所需的所有鼓励和空间。

First, I want to thank my family for their support and understanding. My wife, Diana, and my daughter, Ada, were with me every step of the way, giving me all the encouragement and space I needed to complete this book.

写书绝对是团队的努力。我很感谢迈克尔·斯蒂芬斯 (Michael Stephens) 的初步反馈,正是这些反馈使这本书成为您今天所读的内容。我要感谢我的编辑 Elesha Hyde,感谢她提供的所有帮助、建议和反馈。感谢 Mike Shepard 审阅每一章并让我保持诚实。另外,感谢 German Gonzales 仔细检查每个代码示例并确保一切都按照描述进行。我要感谢所有审稿人花时间并提供宝贵的反馈。感谢 Viktor Bek、Roberto Casadei、Ahmed Chicktay、John Corley、Justin Coulston、Theo Despoudis、David DiMaria、Christopher Fry、German Gonzalez-Morris、Vipul Gupta、Peter Hampton、Clive Harber、Fred Heath、Ryan Huber、Des Horsley、Kevin诺曼·D·卡普昌,

Writing a book is most definitely a team effort. I’m grateful for Michael Stephens’ initial feedback, which helped shape the book into what you are reading today. I want to thank my editor, Elesha Hyde, for all her help, advice, and feedback. Thanks to Mike Shepard for reviewing every chapter and keeping me honest. Also, thanks to German Gonzales for going through each and every code sample and making sure that everything works as described. I want to thank all reviewers for taking their time and providing invaluable feedback. Thanks to Viktor Bek, Roberto Casadei, Ahmed Chicktay, John Corley, Justin Coulston, Theo Despoudis, David DiMaria, Christopher Fry, German Gonzalez-Morris, Vipul Gupta, Peter Hampton, Clive Harber, Fred Heath, Ryan Huber, Des Horsley, Kevin Norman D. Kapchan, Jose San Leandro, James Liu, Wayne Mather, Arnaldo Gabriel Ayala Meyer, Riccardo Noviello, Marco Perone, Jermal Prestwood, Borja Quevedo, Domingo Sebastián Sastre, Rohit Sharm, and Greg Wright.

我要感谢我的同事和导师,感谢他们教给我的一切。当我学习如何利用类型来改进我们的代码库时,我很幸运有一些很棒的、支持我的经理。感谢 Mike Navarro、David Hansen 和 Ben Ross 的信任。

I want to thank my colleagues and mentors for everything they taught me. As I was learning about leveraging types to improve our codebase, I was lucky to have some great, supportive managers. Thanks to Mike Navarro, David Hansen, and Ben Ross for your trust.

感谢整个 C++ 社区,我从中学到了很多东西,尤其要感谢 Sean Parent 鼓舞人心的演讲和宝贵建议。

Thanks to the whole C++ community from which I learned so much and especially to Sean Parent for his inspiring talks and his great advice.

关于本书

About This Book

Programming with Types旨在展示如何使用类型系统来编写更好、更安全的代码。尽管大多数讨论类型系统的书籍都侧重于更正式的方面,但本书采用了务实的方法。它包含您在日常工作中会遇到的大量示例、应用程序和场景。

Programming with Types aims to show how you can use type systems to write better, safer code. Although most books discussing type systems focus on more formal aspects, this book takes a pragmatic approach. It contains numerous examples, applications, and scenarios that you will encounter in your day job.

谁应该读这本书

Who should read this book

本书适用于希望更多地了解类型系统如何工作以及如何使用它们来提高代码质量的实践程序员。您应该具有使用面向对象语言(如 Java、C#、C++ 或 JavaScript/TypeScript)的经验。您还应该具备一些最低限度的软件设计经验。尽管本书将提供各种用于编写健壮、可组合且封装更好的代码的技术,但它假定您知道为什么需要这些属性。

This book is for practicing programmers who want to learn more about how type systems work and how to use them to improve code quality. You should have some experience using an object-oriented language such as Java, C#, C++, or JavaScript/ TypeScript. You should also have some minimum software design experience. Although the book will provide various techniques for writing robust, composable, and better-encapsulated code, it assumes that you know why these properties are desirable.

本书的组织方式:路线图

How this book is organized: a road map

本书共有 11 章,涵盖了类型编程的各个方面:

This book has 11 chapters covering various aspects of programming with types:

  • 第 1 章介绍类型和类型系统,讨论它们存在的原因和用途。我们回顾了类型系统的类型,并讨论了类型强度、静态类型和动态类型。
  • Chapter 1 introduces types and type systems, discussing why they exist and how they are useful. We go over types of type systems and talk about typing strength, static typing, and dynamic typing.
  • 第 2 章介绍了大多数语言通用的基本类型以及使用它们时需要注意的问题。常见的基本类型是空类型和单元类型、布尔值、数字、字符串、数组和引用。
  • Chapter 2 covers basic types common across most languages and gotchas to be aware of when using them. Common basic types are the empty and unit types, Booleans, numbers, strings, arrays, and references.
  • 第 3 章是关于组合的:组合类型以定义新类型的各种方式。本章还展示了实现访问者设计模式的不同方法并定义了代数数据类型。
  • Chapter 3 is about composition: various ways in which types can be combined to define new types. The chapter also shows different ways to implement the visitor design pattern and defines algebraic data types.
  • 第 4 章讨论类型安全——我们如何使用类型来减少歧义和防止错误。本章还展示了如何使用类型转换在代码中添加或删除类型信息。
  • Chapter 4 talks about type safety—how we can use types to reduce ambiguity and prevent errors. The chapter also shows how we can add or remove typing information from our code by using type casting.
  • 第 5 章介绍了函数类型以及当我们有能力创建函数变量时我们可以做什么。本章展示了实现策略模式和状态机的替代方法,并介绍了基本map()filter()、 和reduce()算法。
  • Chapter 5 introduces function types and what we can do when we have the ability to create function variables. The chapter shows alternative ways to implement the strategy pattern and state machines, and introduces the fundamental map(), filter(), and reduce() algorithms.
  • 第 6 章建立在前一章的基础上,展示了函数类型的一些高级应用,从简化的装饰器模式到可恢复函数和异步函数。
  • Chapter 6 builds on the preceding chapter and shows a few advanced applications of function types, from a simplified decorator pattern to resumable functions and asynchronous functions.
  • 第 7 章介绍子类型并讨论类型兼容性。我们查看顶部和底部类型的应用程序,然后从子类型的角度了解求和类型、集合和函数类型如何相互关联。
  • Chapter 7 introduces subtyping and discusses type compatibility. We look at applications of top and bottom types and then see how sum types, collections, and function types relate to one another from a subtyping perspective.
  • 第 8 章讨论了面向对象编程的关键元素以及何时使用每个元素。本章涵盖接口、继承、组合和混合。
  • Chapter 8 talks about the key elements of object-oriented programming and when to use each one. The chapter covers interfaces, inheritance, composition, and mix-ins.
  • 第 9 章介绍泛型编程及其第一个应用:泛型数据结构。通用数据结构将数据布局与数据本身分开;迭代器可以遍历这些数据结构。
  • Chapter 9 introduces generic programming and its first application: generic data structures. Generic data structures separate the layout of the data from the data itself; iterators enable traversal of these data structures.
  • 第 10 章继续泛型编程的主题并讨论泛型算法和迭代器类别。通用算法是我们可以在不同类型的数据中重复使用的算法。迭代器充当数据结构和算法之间的接口,并且根据它们的功能,它们启用不同的算法。
  • Chapter 10 continues the topic of generic programming and discusses generic algorithms and iterator categories. Generic algorithms are algorithms we can reuse across different types of data. Iterators act as an interface between data structures and algorithms, and depending on their capabilities, they enable different algorithms.
  • 第 11 章是最后一章,介绍了更高种类的类型,并解释了什么是仿函数和单子以及如何使用它们。本章最后给出了一些进一步研究的建议。
  • Chapter 11, the final chapter, introduces higher kinded types and explains what functors and monads are and how they can be used. The chapter ends with some pointers for further study.
  • 本书各章建立在前几章介绍的概念之上,因此您应该按顺序阅读它们。话虽如此,本书中有四个相当独立的主要主题。前四章涵盖基础知识;第 5 章和6章涵盖函数类型;第 7 章和8章介绍了子类型;第9、10和11章是关于泛型编程的
  • The chapters in the book build on concepts introduced in earlier chapters, so you should read them in order. That being said, there are four major topics in the book that are fairly independent. The first four chapters cover fundamentals; chapters 5 and 6 cover function types; chapters 7 and 8 cover subtyping; and chapters 9, 10, and 11 are about generic programming.

关于代码

About the code

本书包含许多源代码示例,包括带编号的列表和与普通文本的内联。在这两种情况下,源代码都被格式化为 afixed-width font like this以将其与普通文本分开。有时,代码也会以粗体显示,以突出显示与本章之前的步骤相比发生变化的代码,例如当新功能添加到现有代码行时。

This book contains many examples of source code both in numbered listings and inline with normal text. In both cases, source code is formatted in a fixed-width font like this to separate it from ordinary text. Sometimes, code is also in bold to highlight code that has changed from previous steps in the chapter, such as when a new feature adds to an existing line of code.

在许多情况下,原始源代码已被重新​​格式化;我添加了换行符并修改了缩进以适应书中可用的页面空间。在极少数情况下,即使这样还不够,列表中包含行继续标记 ( )。此外,当在文本中描述代码时,源代码中的注释通常会从列表中删除。许多清单都附有代码注释,突出了重要的概念。

In many cases, the original source code has been reformatted; I’ve added line breaks and reworked indentation to accommodate the available page space in the book. In rare cases, even this was not enough, and listings include line-continuation markers (). Additionally, comments in the source code have often been removed from the listings when the code is described in the text. Code annotations accompany many of the listings, highlighting important concepts.

本书中的所有代码示例都可以在 GitHub 上找到,网址为https://github.com/vladris/programming-with-types/。该代码是使用 TypeScript 3.3 版构建的,针对 ES6 标准,具有严格的设置。

All the code samples in this book are available on GitHub at https://github.com/vladris/programming-with-types/. The code was built with version 3.3 of TypeScript, targeting the ES6 standard, with strict settings.

关于作者

About the author

Vlad Riscutia 是 Microsoft 的一名软件工程师,拥有十多年的经验。在此期间,他领导了多个重大软件项目并指导了许多初级工程师。

Vlad Riscutia is a software engineer at Microsoft with more than a decade of experience. During this time, he has led several major software projects and mentored many junior engineers.

图书论坛

Book forum

购买Programming with Types可以免费访问由 Manning Publications 运营的私人 Web 论坛,您可以在该论坛上对本书发表评论、提出技术问题,并获得作者和其他用户的帮助。要访问该论坛,请转至https://forums.manning.com/forums/programming-with-types您还可以在https://forums.manning.com/forums/about上了解有关 Manning 论坛和行为规则的更多信息。

Purchase of Programming with Types includes free access to a private web forum run by Manning Publications where you can make comments about the book, ask technical questions, and receive help from the author and from other users. To access the forum, go to https://forums.manning.com/forums/programming-with-types. You can also learn more about Manning’s forums and the rules of conduct at https://forums.manning.com/forums/about.

Manning 对我们的读者的承诺是提供一个场所,让各个读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与任何特定数量的承诺,他们对论坛的贡献仍然是自愿的(并且是无偿的)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他的兴趣发生偏差!只要本书还在印刷,就可以从出版商的网站访问论坛和以前讨论的档案。

Manning’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the author can take place. It is not a commitment to any specific amount of participation on the part of the author, whose contribution to the forum remains voluntary (and unpaid). We suggest you try asking the author some challenging questions lest his interest stray! The forum and the archives of previous discussions will be accessible from the publisher’s website as long as the book is in print.

关于封面插图

About the Cover Illustration

圣索弗

Saint-Sauver

Programming with Types封面上的人物标题为“Fille Lipporette en habit de Noce”或“Liporette girl in wedding dress”。插图取自 Jacques Grasset de Saint-Sauveur(1757-1810 年)收集的来自不同国家的服饰,名为Costumes de Différents Pays,1797 年在法国出版。每幅插图均由手工精心绘制和着色。Grasset de Saint-Sauveur 丰富多样的藏品生动地提醒我们 200 年前世界城镇和地区在文化上的差异。人们彼此隔绝,说着不同的方言和语言。无论是在大街上还是在乡下,仅凭着装就很容易辨别出他们住在哪里,他们的行业或生活地位。

The figure on the cover of Programming with Types is captioned “Fille Lipporette en habit de Noce,” or “Liporette girl in wedding dress.” The illustration is taken from a collection of dress costumes from various countries by Jacques Grasset de Saint-Sauveur (1757–1810), titled Costumes de Différents Pays, published in France in 1797. Each illustration is finely drawn and colored by hand. The rich variety of Grasset de Saint-Sauveur’s collection reminds us vividly of how culturally apart the world’s towns and regions were just 200 years ago. Isolated from each other, people spoke different dialects and languages. In the streets or in the countryside, it was easy to identify where they lived and what their trade or station in life was just by their dress.

从那时起,我们的着装方式发生了变化,当时如此丰富的地区多样性已经消失。现在很难区分不同大陆的居民,更不用说不同的城镇、地区或国家了。也许我们已经用文化多样性换取了更加多样化的个人生活——当然是为了更加多样化和快节奏的技术生活。

The way we dress has changed since then and the diversity by region, so rich at the time, has faded away. It is now hard to tell apart the inhabitants of different continents, let alone different towns, regions, or countries. Perhaps we have traded cultural diversity for a more varied personal life—certainly for a more varied and fast-paced technological life.

在这个很难区分计算机书籍的时代,Manning 以两个世纪前区域生活的丰富多样性为基础,由 Grasset de Saint-索沃尔的照片。

At a time when it is hard to tell one computer book from another, Manning celebrates the inventiveness and initiative of the computer business with book covers based on the rich diversity of regional life of two centuries ago, brought back to life by Grasset de Saint-Sauveur’s pictures.

第1章。类型简介

Chapter 1. Introduction to typing

本章涵盖

This chapter covers

  • 为什么存在类型系统
  • Why type systems exist
  • 强类型代码的好处
  • Benefits of strongly typed code
  • 类型系统的类型
  • Types of type systems
  • 类型系统的共同特征
  • Common features of type systems

火星气候轨道飞行器在行星大气层中解体,因为洛克希德公司开发的一个部件产生了以磅力秒(美国单位)为单位的动量测量值,而美国宇航局开发的另一个部件预期以牛顿秒(公制单位)为单位测量动量。对这两种措施使用不同的类型可以避免灾难的发生。

The Mars Climate Orbiter disintegrated in the planet’s atmosphere because a component developed by Lockheed produced momentum measurements in pound-force seconds (U.S. units), whereas another component developed by NASA expected momentum to be measured in Newton seconds (metric units). Using different types for the two measures would have prevented the catastrophe.

正如我们将在本书中看到的那样,类型检查器提供了强大的方法来消除整类错误,前提是它们获得了足够的信息。随着软件复杂性的增加,提供更好的正确性保证的需求也随之增加。监控和测试可以显示软件在给定时间点、给定特定输入时是否按照规范运行。类型为我们提供了更通用的证据,证明无论输入如何,代码都将按照规范运行。

As we will see throughout this book, type checkers provide powerful ways to eliminate whole classes of errors, provided they are given enough information. As software complexity increases, so does the need to provide better correctness guarantees. Monitoring and testing can show that the software is behaving according to spec at a given point in time, given specific input. Types give us more general proofs that the code will behave according to spec regardless of input.

编程语言研究提出了越来越强大的类型系统。(例如,参见 Elm 和 Idris 等语言。)Haskell 越来越受欢迎。与此同时,正在努力将编译时类型检查引入动态类型语言:Python 添加了对类型提示的支持,而 TypeScript 是一种专门为 JavaScript 提供编译时类型检查而创建的语言。

Programming language research is coming up with ever-more-powerful type systems. (See, for example, languages like Elm and Idris.) Haskell is gaining in popularity. At the same time, there are ongoing efforts to bring compile-time type checking to dynamically typed languages: Python added support for type hints, and TypeScript is a language created for the sole purpose of providing compile-time type checking to JavaScript.

键入代码显然是有价值的,利用您的编程语言提供的类型系统的功能将帮助您编写更好、更安全的代码。

There clearly is value in typing code, and leveraging the features of the type systems that your programming languages provide will help you write better, safer code.

1.1. 这本书是为谁而写的

1.1. Whom this book is for

这是一本练习程序员的书。您应该能够熟练地使用 Java、C#、C++ 或 JavaScript/TypeScript 等主流编程语言编写代码。本书中的代码示例使用 TypeScript,但大部分内容与语言无关。事实上,示例并不总是使用惯用的 TypeScript。在可能的情况下,编写代码示例是为了让来自其他语言的程序员可以访问。请参阅附录 A 以了解如何构建本书中的代码示例,以及附录 B 以获取简短的 TypeScript 备忘单。

This is a book for practicing programmers. You should be comfortable writing code in a mainstream programming language like Java, C#, C++, or JavaScript/TypeScript. The code examples in this book are in TypeScript, but most of the content is language-agnostic. In fact, the examples don’t always use idiomatic TypeScript. Where possible, code examples are written to be accessible to programmers coming from other languages. See appendix A for how to build the code samples in this book and appendix B for a short TypeScript cheat sheet.

如果您在日常工作中开发面向对象的代码,您可能听说过代数数据类型 (ADT)、lambda、泛型、函子或 monad,并且想更好地了解它们是什么以及它们与您的代码有何关联工作。

If you are developing object-oriented code at your day job, you might have heard of algebraic data types (ADTs), lambdas, generics, functors, or monads, and would like to better understand what these are and how they are relevant to your work.

本书将教你如何依靠编程语言的类型系统来设计不易出错、组件化程度更高且更易于理解的代码。我们将看到可能在运行时发生并导致整个系统故障的错误如何转化为编译错误并在它们造成任何损害之前被捕获。

This book will teach you how to rely on the type system of your programming language to design code that is less error-prone, better componentized, and easier to understand. We’ll see how errors which could happen at run time and cause an entire system to malfunction can be transformed into compilation errors and caught before they can cause any damage.

许多关于类型系统的文献都是正式的。本书侧重于类型系统的实际应用;因此,数学被保持在最低限度。也就是说,您应该熟悉函数和集合等基本代数概念。我们将依靠这些来解释一些相关的概念。

A lot of the literature on type systems is formal. This book focuses on practical applications of type systems; thus, math is kept to a minimum. That being said, you should be familiar with basic algebra concepts like functions and sets. We will rely on these to explain some of the relevant concepts.

1.2. 为什么存在类型

1.2. Why types exist

在硬件和机器代码的底层,程序逻辑(代码及其操作的数据都表示为位。在此级别,代码和数据之间没有区别,因此当系统将一个误认为另一个时,很容易发生错误。这些错误的范围从程序崩溃到严重的安全漏洞,在这些漏洞中,攻击者“欺骗”系统将其输入数据作为代码执行。

At the low level of hardware and machine code, the program logic (the code) and the data it operates on are both represented as bits. At this level, there is no difference between the code and the data, so errors can easily happen when the system mistakes one for the other. These errors range from program crashes to severe security vulnerabilities in which an attacker “tricks” the system into executing their input data as code.

这种松散解释的一个例子是 JavaScripteval()函数,它将字符串计算为代码。当提供的字符串是有效的 Java-Script 代码时,它运行良好,但如果不是,则会导致运行时错误,如下一个清单所示。

An example of this kind of loose interpretation is the JavaScript eval() function, which evaluates a string as code. It works well when the string provided is valid Java-Script code but causes a run-time error when it isn’t, as shown in the next listing.

清单 1.1。试图将数据解释为代码
console.log(eval("40+2"));           1个

console.log(eval("Hello world!"));   2个
console.log(eval("40+2"));           1

console.log(eval("Hello world!"));   2

  • 1 向控制台打印“42”
  • 1 Prints “42” to the console
  • 2 引发“SyntaxError: unexpected token: identifier”
  • 2 Raises “SyntaxError: unexpected token: identifier”

1.2.1.0和1

1.2.1. 0s and 1s

除了区分代码和数据之外,我们还需要知道如何解释一段数据。16 位序列1100001010100011可以表示无符号 16 位整数49827、有符号 16 位整数-15709、UTF-8 编码字符或完全不同的东西,如图1.1'£'所示。我们的程序运行的硬件将所有内容存储为位序列,因此我们需要一个额外的层来赋予这些数据意义。

Beyond distinguishing between code and data, we need to know how to interpret a piece of data. The 16-bit sequence 1100001010100011 can represent the unsigned 16-bit integer 49827, the signed 16-bit integer -15709, the UTF-8 encoded character '£', or something completely different, as we can see in figure 1.1. The hardware our programs run on stores everything as sequences of bits, so we need an extra layer to give meaning to this data.

图 1.1。比特序列可以用多种方式解释。

类型为这些数据赋予意义,并告诉我们的软件如何在给定的上下文中解释给定的位序列,以便它保留预期的含义。

Types give meaning to this data and tell our software how to interpret a given sequence of bits in a given context so that it preserves the intended meaning.

类型还限制了变量可以采用的有效值集。带符号的 16 位整数可以表示从-32768到的任何整数值32767,但不能表示其他任何整数。限制允许值范围的能力有助于通过不允许无效值在运行时出现来消除整个错误类别,如图1.2所示。将类型视为可能值的集合对于理解本书涵盖的许多概念很重要。

Types also constrain the set of valid values a variable can take. A signed 16-bit integer can represent any integer value from -32768 to 32767 but nothing else. The ability to restrict the range of allowed values helps eliminate whole classes of errors by not allowing invalid values to appear at run time, as shown in figure 1.2. Viewing types as sets of possible values is important to understanding many of the concepts covered in this book.

图 1.2。类型为带符号的 16 位整数的位序列。类型信息(16 位带符号整数)告诉编译器和/或运行时,位序列表示 和 之间的整数值,-32768确保32767正确解释为-15709

正如我们将在 1.3 节中看到的,当我们向代码添加属性时,系统会强制执行许多其他安全属性,例如将值标记为const或将成员标记为private

As we will see in section 1.3, many other safety properties are enforced by the system when we add properties to our code, such as marking a value as const or a member as private.

1.2.2.什么是类型和类型系统?

1.2.2. What are types and type systems?

因为本书讨论的是类型和类型系统,所以让我们在继续之前定义这些术语。

Because this book talks about types and type systems, let’s define these terms before moving forward.

类型

类型是一种数据分类,它定义了可以对该数据执行的操作、数据的含义以及允许值的集合。编译器和/或运行时会检查类型,以确保数据的完整性、实施访问限制并按开发人员的意图解释数据。

A type is a classification of data that defines the operations that can be done on that data, the meaning of the data, and the set of allowed values. Typing is checked by the compiler and/or run time to ensure the integrity of the data, enforce access restrictions, and interpret the data as meant by the developer.

在某些情况下,我们将简化我们的讨论并忽略操作部分,因此我们将类型简单地视为集合,它表示该类型的实例可以采用的所有可能值。

In some cases, we will simplify our discussion and ignore the operations part, so we’ll look at types simply as sets, which represent all the possible values an instance of that type can take.

类型系统

类型系统是一组规则,用于为编程语言的元素分配和强制类型。这些元素可以是变量、函数和其他更高级别的结构。类型系统通过您在代码中提供的符号或通过基于上下文推断特定元素的类型来隐式分配类型。它们允许类型之间的各种转换,而不允许其他类型。

A type system is a set of rules that assigns and enforces types to elements of a programming language. These elements can be variables, functions, and other higher-level constructs. Type systems assign types through notation you provide in the code or implicitly by deducing the type of a certain element based on context. They allow various conversions between types and disallow others.

现在我们已经定义了类型和类型系统,让我们看看如何执行类型系统的规则。图 1.3在较高层次上显示了源代码是如何执行的。

Now that we’ve defined types and type systems, let’s see how the rules of a type system are enforced. Figure 1.3 shows, at a high-level, how source code gets executed.

图 1.3。源代码由编译器或解释器转换为可由运行时执行的代码。运行时是物理计算机或虚拟机,比如Java的JVM,或者浏览器的JavaScript引擎。

在非常高的层次上,我们编写的源代码被编译器或解释器转换为机器指令或运行时指令。这个运行时可以是物理计算机,在这种情况下指令是 CPU 指令,也可以是虚拟机,有自己的指令集和设施。

At a very high level, the source code we write gets transformed by a compiler or interpreter into instructions for a machine, or run time. This run time can be a physical computer, in which case the instructions are CPU instructions, or it can be a virtual machine, with its own instruction set and facilities.

类型检查

类型检查过程确保程序遵守类型系统的规则。这种类型检查由编译器在转换代码时完成,或由运行时在执行代码时完成。处理类型规则执行的编译器组件称为类型检查器

The process of type checking ensures that the rules of the type system are respected by the program. This type checking is done by the compiler when converting the code or by the run time while executing the code. The component of the compiler that handles enforcement of the typing rules is called a type checker.

如果类型检查失败,意味着程序不遵守类型系统的规则,我们将以编译失败或运行时错误告终。我们将在 1.4 节中更详细地讨论编译时类型检查与执行时(或运行时)类型检查之间的区别。

If type checking fails, meaning that the rules of the type system are not respected by the program, we end up with a failure to compile or with a run-time error. We will go over the difference between compile-time type checking versus execution-time (or run-time) type checking in more detail in section 1.4.

类型检查和证明

类型系统背后有很多正式的理论。值得注意的 Curry-Howard 对应关系,也称为程序证明,显示了逻辑与类型理论之间的密切联系。它表明我们可以将类型视为逻辑命题,将一种类型到另一种类型的函数视为逻辑蕴涵。类型的值等同于命题为真的证据。

There is a lot of formal theory behind type systems. The remarkable Curry-Howard correspondence, also known as proofs-as-programs, shows the close connection between logic and type theory. It shows that we can view a type as a logic proposition, and a function from one type to another as a logic implication. A value of a type is equivalent to evidence that the proposition is true.

获取一个函数,该函数接收 a 作为参数boolean并返回 a string

Take a function that receives as argument a boolean and returns a string.

布尔值到字符串

Boolean to string

函数 booleanToString(b: 布尔值): 字符串 {
    如果(二){
        返回“真”;
    } 别的 {
        返回“假”;
    }
}
function booleanToString(b: boolean): string {
    if (b) {
        return "true";
    } else {
        return "false";
    }
}

这个功能也可以理解为“boolean暗示string”。给定命题的证据boolean,这个函数(蕴涵)可以产生命题的证据string。的证据boolean是该类型的值,true或者false。当我们有了它时,这个函数(蕴含)将为我们提供 as 的证据stringas 要么 string"true"要么 string "false"

This function can also be interpreted as “boolean implies string.” Given evidence of the proposition boolean, this function (implication) can produce evidence of the proposition string. Evidence of boolean is a value of that type, true or false. When we have that, this function (implication) will give us evidence of string as either the string "true" or the string "false".

逻辑与类型理论之间的密切关系表明,尊重类型系统规则的程序等同于逻辑证明。换句话说,类型系统是我们用来编写这些证明的语言。Curry-Howard 对应关系很重要,因为它为保证程序正确运行提供了逻辑严谨性。

The close relationship between logic and type theory shows that a program that respects the type system rules is equivalent to a logic proof. In other words, the type system is the language in which we write these proofs. The Curry-Howard correspondence is important because it brings logic rigor to the guarantees that a program will behave correctly.

1.3. 类型系统的好处

1.3. Benefits of type systems

因为最终数据都是 0 和 1,所以数据的属性,例如如何解释它、它是否不可变以及它的可见性,都是类型级别的属性。我们将变量声明为数字,类型检查器确保我们不会将其数据解释为字符串。我们将变量声明为私有或只读,虽然数据本身在内存与公共可变数据没有什么不同,类型检查器可以确保我们不会在其范围之外引用私有变量或尝试更改只读数据。

Because ultimately data is all 0s and 1s, properties of the data, such as how to interpret it, whether it is immutable, and its visibility, are type-level properties. We declare a variable as a number, and the type checker ensures that we don’t interpret its data as a string. We declare a variable as private or read-only, and although the data itself in memory is no different from public mutable data, the type checker can make sure we do not refer to a private variable outside its scope or try to change read-only data.

类型化的主要好处是正确性不变性封装性可组合性可读性。这五个都是良好软件设计和行为的基本特征。系统随着时间的推移而发展。这些特征抵消了不可避免地试图潜入系统的熵。

The main benefits of typing are correctness, immutability, encapsulation, composability, and readability. All five are fundamental features of good software design and behavior. Systems evolve over time. These features counterbalance the entropy that inevitably tries to creep into the system.

1.3.1.正确性

1.3.1. Correctness

正确的代码意味着代码根据其规范运行,产生预期的结果而不会产生运行时错误或崩溃。类型帮助我们对代码增加更多的严格性,以确保它的行为正确。

Correct code means code that behaves according to its specification, producing expected results without creating run-time errors or crashes. Types help us add more strictness to the code to ensure that it behaves correctly.

例如,假设我们想"Script"在另一个字符串中找到该字符串的索引。在不提供足够的类型信息的情况下,我们可以允许类型的值any作为参数传递给我们的函数。如果参数不是字符串,我们将遇到运行时错误,如下一个清单所示。

As an example, let’s say we want to find the index of the string "Script" within another string. Without providing enough type information, we can allow a value of any type to be passed as an argument to our function. We are going to hit run-time errors if the argument is not a string, as the next listing shows.

清单 1.2。类型信息不足
函数 scriptAt(s: any): number {        1
    返回 s.indexOf("脚本");
}

console.log(scriptAt("TypeScript"));      2 
console.log(scriptAt(42));                3个
function scriptAt(s: any): number {       1
    return s.indexOf("Script");
}

console.log(scriptAt("TypeScript"));      2
console.log(scriptAt(42));                3

  • 1 参数 s 的类型为 any,允许任何类型的值。
  • 1 Argument s has type any, which allows a value of any type.
  • 2 该行正确地将“4”打印到控制台。
  • 2 This line correctly prints “4” to the console.
  • 3 将数字作为参数传递会导致运行时类型错误。
  • 3 Passing a number as an argument causes a run-time TypeError.

该程序不正确,因为它42不是scriptAt函数的有效参数,但编译器没有拒绝它,因为我们没有提供足够的类型信息。string让我们通过将参数限制为下一个清单中 类型的值来优化代码。

The program is incorrect, as 42 is not a valid argument to the scriptAt function, but the compiler did not reject it because we hadn’t provided enough type information. Let’s refine the code by constraining the argument to a value of type string in the next listing.

清单 1.3。细化类型信息
函数 scriptAt(s: string ): number {     1
    返回 s.indexOf("脚本");
}

console.log(scriptAt("TypeScript"));
console.log(scriptAt(42));                2个
function scriptAt(s: string): number {    1
    return s.indexOf("Script");
}

console.log(scriptAt("TypeScript"));
console.log(scriptAt(42));                2

  • 1 参数 s 现在具有字符串类型。
  • 1 Argument s now has type string.
  • 2 由于类型不匹配,代码无法在此行编译。
  • 2 Code fails to compile at this line due to type mismatch.

现在错误的程序被编译器拒绝并显示以下错误消息:

Now the incorrect program is rejected by the compiler with this error message:

“42”类型的参数不可分配给“字符串”类型的参数
Argument of type '42' is not assignable to parameter of type 'string'

利用类型系统,我们将过去可能在生产中遇到并影响我们客户的运行时问题转变为我们必须在部署代码之前解决的无害编译时问题。类型检查器确保我们永远不会尝试将苹果作为橙子传递;因此,我们的代码变得更加健壮。

Leveraging the type system, we transformed what used to be a run-time issue that could have been hit in production, affecting our customers, into a harmless compile-time issue that we must fix before deploying our code. The type checker makes sure we never try to pass apples as oranges; thus, our code becomes more robust.

当程序进入错误状态时会发生错误,这意味着无论出于何种原因,其所有活动变量的当前组合都是无效的。消除其中一些不良状态的一种技术是通过限制变量可能取值的数量来减少状态空间,如图1.4所示。

Errors occur when a program gets into a bad state, which means that the current combination of all its live variables is invalid for whatever reason. One technique for eliminating some of these bad states is reducing the state space by constraining the number of possible values that variables can take, like in figure 1.4.

图 1.4。正确声明类型,我们可以禁止无效值。第一种类型过于宽松,允许我们不想要的值。如果代码试图将不需要的值分配给变量,则第二种限制性更强的类型将无法编译。

我们可以将正在运行的程序的状态空间定义为其所有活动变量的所有可能值的组合。即每个变量类型的笛卡尔积。请记住,类型可以被视为变量的一组可能值。两个集合的笛卡尔积是由两个集合中的所有有序对组成的集合。

We can define the state space of a running program as the combination of all possible values of all its live variables. That is, the Cartesian product of the type of each variable. Remember, a type can be viewed as a set of possible values for a variable. The Cartesian product of two sets is the set comprised of all ordered pairs from the two sets.

安全

不允许潜在不良状态的一个重要副产品是更安全的代码。许多攻击依赖于执行用户提供的数据、缓冲区溢出和其他此类技术,通常可以通过足够强大的类型系统和良好的类型定义来缓解这些攻击。

An important byproduct of disallowing potential bad states is more secure code. Many attacks rely on executing user-provided data, buffer overruns, and other such techniques, which can often be mitigated with a strong-enough type system and good type definitions.

代码正确性不仅仅是消除代码中的无辜错误,还包括防止恶意攻击。

Code correctness goes beyond eliminating innocent bugs in the code to preventing malicious attacks.

1.3.2.不变性

1.3.2. Immutability

不变性是另一个与将我们的运行系统视为在其状态空间中移动密切相关的属性。当我们处于已知良好的状态时,如果我们能够保持该状态的某些部分不发生变化,我们就可以减少出错的可能性。

Immutability is another property closely related to viewing our running system as moving through its state space. When we are in a known-good state, if we can keep parts of that state from changing, we reduce the possibility of errors.

让我们举一个简单的例子,我们试图通过0检查除数的值来防止除法,如果除数是 则抛出错误0,如以下清单所示。如果我们检查后值可以改变,则支票不是很有价值。

Let’s take a simple example in which we attempt to prevent division by 0 by checking the value of our divisor and throwing an error if the divisor is 0, as shown in the following listing. If the value can change after we inspect it, the check is not very valuable.

清单 1.4。坏突变
函数 safeDivide(): 数字 {
    让 x: 数字 = 42;

     if (x == 0) throw new Error("x should not be 0");    1个

    x = x - 42;                                           2个

    返回 42/x;                                        3 
}
function safeDivide(): number {
    let x: number = 42;

     if (x == 0) throw new Error("x should not be 0");    1

    x = x - 42;                                           2

    return 42 / x;                                        3
}

  • 1 检查 x 是否有效。
  • 1 Check if x is valid.
  • 2 Bug:检查后x变为0。
  • 2 Bug: x becomes 0 after the check.
  • 3 除以 0 结果为无穷大。
  • 3 Division by 0 results in Infinity.

这种情况在实际程序中一直以微妙的方式发生:一个变量被不同的线程同时更改,或者被另一个调用的函数模糊地更改。就像在这个例子中一样,一旦一个值发生变化,我们就会失去我们希望从我们执行的检查中获得的任何保证。作为x一个常量,当我们试图在下一个清单中改变它时,我们会得到一个编译错误。

This happens all the time in real programs, in subtle ways: a variable gets changed concurrently by a different thread or obscurely by another called function. Just as in this example, as soon as a value changes, we lose any guarantees we were hoping to get from the checks we performed. Making x a constant, we get a compilation error when we try to mutate it in the next listing.

清单 1.5。不变性
function safeDivide(): number {
     const x: number = 42;                1个

    if (x == 0) throw new Error("x should not be 0");

    x = x - 42;                          2个

    返回 42/x;
}
function safeDivide(): number {
    const x: number = 42;                1

    if (x == 0) throw new Error("x should not be 0");

    x = x - 42;                          2

    return 42 / x;
}

  • 1 x 是使用关键字 const 而不是关键字 let 来声明的。
  • 1 x is declared using the keyword const instead of the keyword let.
  • 2 此行不再编译,因为 x 是不可变的并且无法重新分配。
  • 2 This line no longer compiles as x is immutable and cannot be reassigned.

该错误被编译器拒绝并显示以下错误消息:

The bug is rejected by the compiler with the following error message:

无法分配给“x”,因为它是常量。
Cannot assign to 'x' because it is a constant.

就内存表示而言,可变和不可变之间没有区别x。constness 属性仅对编译器有意义。它是由类型系统启用的属性。

In terms of in-memory representation, there is no difference between a mutable and an immutable x. The constness property is meaningful only for the compiler. It is a property enabled by the type system.

通过将符号添加到我们的类型来标记不应该改变的状态const可以防止我们失去之前保证的那种突变检查。当涉及并发时,不变性特别有用,因为如果数据不可变,数据竞争就变得不可能。

Marking state that shouldn’t change as such by adding the const notation to our type prevents the kind of mutations with which we lose guarantees we previously checked for. Immutability is especially useful when concurrency is involved, as data races become impossible if data is immutable.

优化编译器在处理不可变变量时可以发出更高效的代码,因为它们的值可以内联。一些函数式编程语言使所有数据不可变:一个函数将一些数据作为输入并返回其他数据而不改变其输入。在这种情况下,当我们验证一个变量并确认它处于良好状态时,我们可以保证它在整个生命周期内都处于良好状态。当然,代价是当我们可以就地操作数据时,我们最终会复制数据,这并不总是可取的。

Optimizing compilers can emit more-efficient code when dealing with immutable variables, as their values can be inlined. Some functional programming languages make all data immutable: a function takes some data as input and returns other data without ever changing its input. In such cases, when we validate a variable and confirm that it is in a good state, we are guaranteed it will be in a good state for its whole lifetime. The trade-off, of course, is that we end up copying data when we could have operated on it in-place, which is not always desirable.

使一切不可变可能并不总是可行的。话虽这么说,尽可能多地使数据不可变将极大地减少诸如不满足先决条件和数据竞争等问题的机会。

Making everything immutable might not always be feasible. That being said, making as much of the data immutable as you reasonably can will tremendously reduce the opportunity for issues such as preconditions not being met and data races.

1.3.3.封装

1.3.3. Encapsulation

封装是隐藏我们代码的某些内部结构的能力,无论是函数、类还是模块。您可能知道,封装是可取的,因为它可以帮助我们处理复杂性:我们将代码拆分为更小的组件,每个组件仅向外界公开严格需要的内容,而其实现细节则保持隐藏和隔离。

Encapsulation is the ability to hide some of the internals of our code, be it a function, a class, or a module. As you probably know, encapsulation is desirable, as it helps us deal with complexity: we split the code into smaller components, and each component exposes only what is strictly needed to the outside world, while its implementation details are kept hidden and isolated.

在下一个清单中,让我们将安全除法示例扩展到一个试图确保除法永远0不会发生的类。

In the next listing, let’s extend our safe division example to a class that tries to ensure that division by 0 never happens.

清单 1.6。封装不够
类 SafeDivisor {
    除数:数字= 1;

    设置除数(值:数字){
        if (value == 0) throw new Error("Value should not be 0");    1个

        this.divisor = 值;
    }

    除法(x:数字):数字{
        返回 x / this.divisor;                                     2个
    }
}

函数利用():数字{
    让 sd = new SafeDivisor();

    sd.divisor = 0;                                                  3
    返回 sd.divide(42);                                            4 
}
class SafeDivisor {
    divisor: number = 1;

    setDivisor(value: number) {
        if (value == 0) throw new Error("Value should not be 0");    1

        this.divisor = value;
    }

    divide(x: number): number {
        return x / this.divisor;                                     2
    }
}

function exploit(): number {
    let sd = new SafeDivisor();

    sd.divisor = 0;                                                  3
    return sd.divide(42);                                            4
}

  • 1 通过在分配前检查值确保除数不会变为 0
  • 1 Ensure that divisor does not become 0 by checking value before assigning
  • 2 除以 0 永远不应该发生。
  • 2 Division by 0 should never happen.
  • 3 因为除数成员是公开的,所以可以绕过检查。
  • 3 Because the divisor member is public, the check can be bypassed.
  • 4 除以 0 返回无穷大。
  • 4 Division by 0 returns Infinity.

在这种情况下,我们不能再使除数不可变,因为我们确实希望 API 的调用者能够更新它。问题是调用者可以绕过0检查并直接设置divisor为任何值,因为它对他们可见。这种情况下的解决方法是将其标记为private并将其范围限定为类,如以下清单所示。

In this case we can no longer make the divisor immutable, as we do want to give callers of our API the ability to update it. The problem is that callers can bypass the 0 check and directly set divisor to any value because it is visible to them. The fix in this case is to mark it as private and scope it to the class, as the following listing shows.

清单 1.7。封装
class SafeDivisor {
    私有除数:number = 1;             1个

    设置除数(值:数字){
        if (value == 0) throw new Error("Value should not be 0");

        this.divisor = 值;
    }

    除法(x:数字):数字{
        返回 x / this.divisor;
    }
}

函数利用(){
    让 sd = new SafeDivisor();

    sd.divisor = 0;                          2个
    sd.divide(42);
}
class SafeDivisor {
    private divisor: number = 1;             1

    setDivisor(value: number) {
        if (value == 0) throw new Error("Value should not be 0");

        this.divisor = value;
    }

    divide(x: number): number {
        return x / this.divisor;
    }
}

function exploit() {
    let sd = new SafeDivisor();

    sd.divisor = 0;                          2
    sd.divide(42);
}

  • 1 名 成员现已标记为私人。
  • 1 Member is now marked as private.
  • 2 此行编译失败,因为不能再在类外部引用除数。
  • 2 This line fails to compile as divisor can no longer be referenced outside the class.

Apublicprivate成员具有相同的内存表示;有问题的代码在第二个示例中不再编译的事实仅仅是由于我们提供的类型符号。事实上,publicprivate和其他可见性类型是它们出现的类型的属性。

A public and a private member have the same in-memory representation; the fact that the problematic code no longer compiles in the second example is simply due to the type notations we provided. In fact, public, private, and other visibility kinds are properties of the type in which they appear.

封装或信息隐藏使我们能够跨公共接口和非公共实现拆分逻辑和数据。这在大型系统中非常有用,因为针对接口(或抽象)工作可以减少理解一段特定代码的作用所需的脑力劳动。我们只需要了解和推理组件的接口,而不是它们的所有实现细节。它还通过在边界内确定非公开信息的范围来提供帮助,并保证外部代码无法修改它,因为它根本无法访问它。

Encapsulation, or information hiding, enables us to split logic and data across a public interface and a nonpublic implementation. This is extremely helpful in large systems, as working against interfaces (or abstractions) reduces the mental effort it takes to understand what a particular piece of code does. We need to understand and reason about only the interfaces of components, not all their implementation details. It also helps by scoping nonpublic information within a boundary and guarantees that external code cannot modify it, as it simply does not have access to it.

封装出现在多个层次:服务将其 API 作为接口公开,模块导出其接口并隐藏实现细节,类仅公开其公共成员,等等。就像嵌套娃娃一样,代码的两部分之间的关​​系越弱,它们共享的信息就越少。这加强了组件可以对其内部管理的数据做出的保证,因为不允许外部代码在不通过组件接口的情况下修改它。

Encapsulation appears at multiple layers: a service exposes its API as an interface, a module exports its interface and hides implementation details, a class exposes only its public members, and so on. Like nesting dolls, the weaker the relationship between two parts of the code, the less information they share. This strengthens the guarantees a component can make about the data it manages internally, as no outside code can be allowed to modify it without going through the component’s interface.

1.3.4.可组合性

1.3.4. Composability

假设我们要在数字数组中找到第一个负数,并在字符串数组中找到第一个单字符字符串。如果不考虑如何将这个问题分解成可组合的部分并将它们重新组合成一个可组合的系统,我们最终可能会得到两个函数:findFirstNegativeNumber()findFirstOneCharacterString(),如以下清单所示。

Let’s say we want to find the first negative number in an array of numbers and the first one-character string in an array of strings. Without thinking about how we can break down this problem into composable pieces and put them back together into a composable system, we could end up with two functions: findFirstNegativeNumber() and findFirstOneCharacterString(), as shown in the following listing.

清单 1.8。不可组合的系统
函数 findFirstNegativeNumber(numbers: number[])
    : 号码 | 不明确的 {
    对于(让我的数字){
        如果 (i < 0) 返回 i;
    }
}

函数 findFirstOneCharacterString(字符串:字符串 [])
    : 串 | 不明确的 {
    for (let str of strings) {
        如果 (str.length == 1) 返回 str;
    }
}
function findFirstNegativeNumber(numbers: number[])
    : number | undefined {
    for (let i of numbers) {
        if (i < 0) return i;
    }
}

function findFirstOneCharacterString(strings: string[])
    : string | undefined {
    for (let str of strings) {
        if (str.length == 1) return str;
    }
}

这两个函数分别搜索第一个负数和第一个单字符字符串。如果没有找到这样的元素,则函数返回undefined(隐含地,通过在没有语句的情况下退出函数return)。

The two functions search for the first negative number and for the first one-character string, respectively. If no such element is found, the functions return undefined (implicitly, by exiting the function without a return statement).

如果出现新要求,我们也应该在找不到元素时记录错误,我们需要更新这两个函数,如下一个清单所示。

If a new requirement comes in that we should also log an error whenever we fail to find an element, we need to update both functions, as shown in the next listing.

清单 1.9。不可组合的系统更新
函数 findFirstNegativeNumber(numbers: number[])
    : 号码 | 不明确的 {
    对于(让我的数字){
        如果 (i < 0) 返回 i;
    }
    console.error("没有找到匹配的值");
}

函数 findFirstOneCharacterString(字符串:字符串 [])
    : 串 | 不明确的 {
    for (let str of strings) {
        如果 (str.length == 1) 返回 str;
    }
    console.error("没有找到匹配的值"); 
}
function findFirstNegativeNumber(numbers: number[])
    : number | undefined {
    for (let i of numbers) {
        if (i < 0) return i;
    }
    console.error("No matching value found");
}

function findFirstOneCharacterString(strings: string[])
    : string | undefined {
    for (let str of strings) {
        if (str.length == 1) return str;
    }
    console.error("No matching value found");
}

这已经不太理想了。如果我们忘记在所有地方应用更新怎么办?这些问题在大型系统中更加复杂。仔细观察每个函数的作用,我们可以看出算法是相同的;但在一种情况下,我们在一个条件下对数字进行操作,而在另一种情况下,我们在不同条件下对字符串进行操作。我们可以提供一个通用算法,该算法根据其操作的类型和检查的条件进行参数化,如以下清单所示。这样的算法不依赖于系统的其他部分,我们可以孤立地对其进行推理。

This is already less than ideal. What if we forget to apply the update everywhere? Such issues compound in large systems. Looking more closely at what each function does, we can tell that the algorithm is the same; but in one case, we operate on numbers with one condition, and in the other, we operate on strings with a different condition. We can provide a generic algorithm parameterized on the type it operates on and the condition it checks for, as shown in the following listing. Such an algorithm does not depend on the other parts of the system, and we can reason about it in isolation.

清单 1.10。组合系统
first<T>(range: T[], p: (elem: T) => 布尔值)
    : 吨 | 不明确的 {
    for (let elem of range) {
        如果(p(元素))返回元素;
    }
}

函数 findFirstNegativeNumber(numbers: number[])
    : 号码 | 不明确的 {
    先返回(数字,n => n < 0);
}

函数 findFirstOneCharacterString(字符串:字符串 [])
    : 串 | 不明确的 {
    首先返回(字符串,str => str.length == 1);
}
function first<T>(range: T[], p: (elem: T) => boolean)
    : T | undefined {
    for (let elem of range) {
        if (p (elem)) return elem;
    }
}

function findFirstNegativeNumber(numbers: number[])
    : number | undefined {
    return first(numbers, n => n < 0);
}

function findFirstOneCharacterString(strings: string[])
    : string | undefined {
    return first(strings, str => str.length == 1);
}

如果这个语法看起来有点奇怪,请不要担心;我们将n => n < 0第 5 章介绍内联函数,在第 9章和第 10介绍泛型。

Don’t worry if the syntax of this looks a bit strange; we’ll cover inline functions such as n => n < 0 in chapter 5 and generics in chapters 9 and 10.

如果我们想向这个实现中添加日志记录,我们只需要更新first. 更好的是,如果我们想出一个更有效的算法,只需更新实现就能使所有调用者受益。

If we want to add logging to this implementation, we need only to update the implementation of first. Better still, if we figure out a more efficient algorithm, simply updating the implementation benefits all callers.

正如我们将在第 10 章讨论泛型算法和迭代器时了解到的那样,我们可以使这个函数更加通用。目前,它只对某种类型的数组进行操作T。它可以扩展到遍历任何数据结构。

As we’ll learn in chapter 10 when we discuss generic algorithms and iterators, we can make this function even more general. Currently, it only operates on an array of some type T. It can be extended to traverse any data structure.

如果代码不可组合,我们需要为每种数据类型、数据结构和条件提供不同的功能,即使它们从根本上实现了相同的抽象。具有抽象然后混合和匹配组件的能力减少了很多重复。通用类型使我们能够表达这些类型的抽象。

If the code is not composable, we need a different function for each data type, data structure, and condition, even though they all fundamentally implement the same abstraction. Having the ability to abstract and then mix and match components reduces a lot of duplication. Generic types enable us to express these kinds of abstractions.

具有组合独立组件的能力可以产生模块化系统和更少的代码来维护。随着代码大小和组件数量的增加,可组合性变得很重要。在可组合系统中,各个部分是松散耦合的;同时,代码不会在每个子系统中重复。通常可以通过更新单个组件而不是在整个系统中进行大量更改来合并新需求,同时了解这样的系统需要更少的思考,因为我们可以孤立地对其部分进行推理。

Having the ability to combine independent components yields a modular system and less code to maintain. Composability becomes important as the size of the code and the number of components increase. In a composable system, the parts are loosely coupled; at the same time, code does not get duplicated in each subsystem. New requirements can usually be incorporated by updating a single component instead of making large changes across the whole system, at the same time understanding that such a system requires less thought, as we can reason about its parts in isolation.

1.3.5.可读性

1.3.5. Readability

代码被阅读的次数比编写的次数多得多。类型化清楚地表明了函数对其参数的期望、通用算法的先决条件是什么、类实现了哪些接口等等。这些信息很有价值,因为我们可以单独推理可读代码:仅通过查看定义,我们应该能够轻松理解代码应该如何工作,而无需浏览源代码来查找调用者和被调用者。

Code is read many more times than it is written. Typing makes it clear what a function expects from its arguments, what the prerequisites for a generic algorithm are, what interfaces a class implements, and so on. This information is valuable because we can reason about readable code in isolation: just by looking at a definition, we should be able to easily understand how the code is supposed to work without having to navigate the sources to find callers and callees.

命名和注释也是其中的重要部分,但是键入会增加另一层信息,因为它允许我们命名约束。让我们看看find()下面清单中的无类型函数声明。

Naming and comments are important parts of this, too, but typing adds another layer of information, as it allows us to name constraints. Let’s look at an untyped find() function declaration in the following listing.

清单 1.11。无类型查找()
声明函数 find(range: any, pred: any): any;
declare function find(range: any, pred: any): any;

只看这个函数,很难说出它需要什么样的参数。我们需要阅读实现,传递我们最好的猜测,看看我们是否遇到运行时错误,或者希望文档涵盖这一点。

Just looking at this function, it’s hard to tell what kind of arguments it expects. We need to read the implementation, pass in our best guess, and see whether we get a run-time error or hope that the documentation covers this.

将以下代码与前面的声明进行对比。

Contrast the following code with the previous declaration.

清单 1.12。键入find()
先声明函数<T>(range: T[],
    p: (elem: T) => 布尔值: T | 不明确的;
declare function first<T>(range: T[],
    p: (elem: T) => boolean): T | undefined;

阅读这个声明,我们看到对于任何类型T,我们需要提供一个数组T[]作为range参数和一个接受 aT并返回 aboolean作为 -p参数的函数。我们还可以立即看到该函数将返回Tor - undefined

Reading this declaration, we see that for any type T, we need to provide an array T[] as the range argument and a function that takes a T and returns a boolean as the -p argument. We can also immediately see that the function is going to return a T or -undefined.

无需查找实现或查找文档,只需阅读此声明即可准确告诉我们要传递的参数类型并减少我们的认知负担,因为我们可以将其视为一个独立的独立实体。明确显示此类类型信息,不仅可供编译器使用,也可供开发人员使用,这使得理解代码变得容易得多。

Instead of having to find the implementation or look up the documentation, just reading this declaration tells us exactly what type of arguments to pass and reduces our cognitive load, as we can treat it as a self-contained, separate entity. Having such type information explicit, available not only to the compiler but also to the developer, makes understanding the code a lot easier.

大多数现代语言都提供某种程度的类型推断,这意味着根据上下文推断变量的类型。这很有用,因为它可以节省我们的冗余输入,但是当编译器可以轻松理解代码而人们这样做太费力时,就会成为一个问题。拼写出来的类型比注释更有价值,因为它是由编译器强制执行的。

Most modern languages provide some level of type inference, which means deducing the type of a variable based on context. This is useful, as it saves us redundant typing, but becomes a problem when the compiler can understand the code easily while it becomes too effortful for people to do so. A spelled-out type is much more valuable than a comment, as it is enforced by the compiler.

1.4. 类型系统的类型

1.4. Types of type systems

如今,大多数语言和运行时都提供某种形式的类型。我们很久以前就意识到,能够将代码解释为数据,将数据解释为代码可能会导致灾难性的后果。结果。现代类型系统之间的主要区别在于何时检查类型以及检查的严格程度。

Nowadays, most languages and run times provide some form of typing. We realized long ago that being able to interpret code as data and data as code can lead to catastrophic results. The main distinction between contemporary type systems lies in when types get checked and how strict the checks are.

对于静态类型,类型检查是在编译时执行的,因此当编译完成时,运行时值可以保证具有正确的类型。另一方面,动态类型将类型检查推迟到运行时,因此类型不匹配会变成运行时错误。

With static typing, type checking is performed at compile time, so when compilation is done, the run-time values are guaranteed to have correct types. Dynamic typing, on the other hand, defers type checking to the run time, so type mismatches become run-time errors.

强类型几乎不进行任何隐式类型转换,而较弱的类型系统允许进行更多的隐式类型转换。

Strong typing does few if any implicit type conversions, whereas weaker type systems allow more implicit type conversions.

1.4.1.动态和静态类型

1.4.1. Dynamic and static typing

JavaScript 是动态类型的,而 TypeScript 是静态类型的。事实上,创建 TypeScript 是为了向 JavaScript 添加静态类型检查。将运行时错误转化为编译错误,尤其是在大型应用程序中,可以使代码更易于维护和恢复。本书侧重于静态类型和静态类型语言,但理解替代方案是很好的。

JavaScript is dynamically typed, and TypeScript is statically typed. In fact, TypeScript was created to add static type checking to JavaScript. Converting what would otherwise be run-time errors to compilation errors, especially in large applications, makes code more maintainable and resilient. This book focuses on static typing and statically typed languages, but it’s good to understand the alternative.

动态类型不会在编译时强加任何类型约束。俗称鸭子类型来自于“如果它像鸭子一样蹒跚而行并且像鸭子一样嘎嘎叫,那么它一定是鸭子”。代码可以尝试以任何它想要的方式自由使用变量,并且键入由运行时应用。我们可以使用any关键字在 TypeScript 中模拟动态类型,它允许无类型变量。

Dynamic typing does not impose any typing constraints at compile time. The colloquial name duck typing comes from the phrase “If it waddles like a duck and it quacks like a duck, it must be a duck.” Code can attempt to freely use a variable in any way it wants, and typing is applied by the run time. We can simulate dynamic typing in TypeScript by using the any keyword, which allows untyped variables.

我们可以实现一个quacker()函数,它接受一个duck类型的参数any并调用quack()它。只要我们向它传递一个具有quack()方法的对象,一切正常。另一方面,如果我们传递一些不能传递的东西quack(),我们会得到一个运行时TypeError,如以下清单所示。

We can implement a quacker() function that takes a duck argument of type any and calls quack() on it. As long as we pass it an object that has a quack() method, everything works. If, on the other hand, we pass something that can’t quack(), we get a run-time TypeError, as shown in the following listing.

清单 1.13。动态类型
功能嘎嘎(鸭子:任何){                                  1
    鸭子嘎嘎();
}

嘎嘎({ 嘎嘎: function () { console.log("嘎嘎"); } });    2
嘎嘎(42);                                                  3个
function quacker(duck: any) {                                 1
    duck.quack();
}

quacker({ quack: function () { console.log("quack"); } });    2
quacker(42);                                                  3

  • 1 该函数接受一个类型为 any 的参数,因此它绕过了编译时类型检查。
  • 1 The function takes an argument of type any, so it bypasses compile-time type checking.
  • 2 我们传递一个带有 quack() 方法的对象,因此调用会打印“quack”。
  • 2 We pass an object with a quack() method, so the call prints “quack.”
  • 3 这会导致运行时错误:TypeError: duck.quack is not a function。
  • 3 This causes a run-time error: TypeError: duck.quack is not a function.

另一方面,静态类型在编译时执行类型检查,因此尝试传递错误类型的参数会导致编译错误。为了利用 TypeScript 的静态类型特性,我们可以通过声明一个接口并正确输入函数的参数来更新代码,如清单 1.14Duck所示。请注意,在 TypeScript 中,我们不必显式声明我们正在实现接口。只要我们提供一个函数,编译器就会认为Duckquack()要实现的接口。在其他语言中,我们必须通过将类声明为实现接口来显式声明。

Static typing, on the other hand, performs type checks at compile time, so attempting to pass an argument of the wrong type causes a compilation error. To leverage the static typing features of TypeScript, we can update the code by declaring a Duck interface and properly typing the function’s argument, as shown in listing 1.14. Note that in TypeScript, we do not have to explicitly declare that we are implementing the Duck interface. As long as we provide a quack() function, the compiler considers the interface to be implemented. In other languages, we would have to be explicit by declaring a class as implementing the interface.

清单 1.14。静态类型
接口鸭{                                            1
    嘎嘎():无效;
}

功能嘎嘎(鸭子:鸭子){                              2
    鸭子嘎嘎();
}

嘎嘎({ 嘎嘎: function () { console.log("嘎嘎"); } });
庸医(42);                                               3个
interface Duck {                                           1
    quack(): void;
}

function quacker(duck: Duck) {                             2
    duck.quack();
}

quacker({ quack: function () { console.log("quack"); } });
quacker(42);                                               3

  • 1 我们期望具有 quack() 方法的对象的接口声明
  • 1 Interface declaration for an object we expect has a quack() method
  • 2 更新的函数现在需要 Duck 类型的参数。
  • 2 Updated function now requires an argument of type Duck.
  • 3 编译错误:“42”类型的参数不可分配给“Duck”类型的参数。
  • 3 Compile error: Argument of type ‘42’ is not assignable to parameter of type ‘Duck’.

在编译时捕获这些类型的错误,在它们导致正在运行的程序出现故障之前,是静态类型的主要好处。

Catching these types of errors at compile time, before they can cause a running program to malfunction, is the key benefit of static typing.

1.4.2.弱类型和强类型

1.4.2. Weak and strong typing

我们经常听到用强类型弱类型来描述类型系统。类型系统的强度描述了系统在执行类型约束方面的严格程度。弱类型系统隐式尝试将值从其实际类型转换为使用该值时预期的类型。

We often hear the terms strong typing and weak typing to describe a type system. The strength of a type system describes how strict the system is with regard to enforcing type constraints. A weak type system implicitly tries to convert values from their actual types to the types expected when the value is used.

考虑这个问题:牛奶是否等于白色?在强类型世界中,不,牛奶是液体,将它与颜色进行比较是没有意义的。在弱类型的世界中,我们可以说,“嗯,牛奶的颜色是白色的,所以是的,它确实等于白色。” 在强类型世界中,我们可以通过使问题更明确来明确地将牛奶转换为颜色:牛奶的颜色是否等于白色?在弱类型的世界中,我们不需要这种细化。

Consider this question: Does milk equal white? In a strongly typed world, no, milk is a liquid, and it makes no sense to compare it to a color. In a weakly typed world, we can say, “Well, milk’s color is white, so yes, it does equal white.” In the strongly typed world, we can explicitly convert milk to a color by making the question more explicit: Does the color of milk equal white? In the weakly typed world, we don’t need this refinement.

JavaScript 是弱类型的。我们可以通过使用anyTypeScript 中的类型并推迟到 JavaScript 来处理运行时的输入来看到这一点。JavaScript 提供了两个相等运算符: ==,它检查两个值是否相等,以及===,它检查值和值的类型是否相等,如下一个清单所示。因为 JavaScript 是弱类型的,所以表达式"42" == 42true. 这是令人惊讶的,因为"42"是文本,而42是数字。

JavaScript is weakly typed. We can see this by using the any type in TypeScript and deferring to JavaScript to handle typing at run time. JavaScript provides two equality operators: ==, which checks whether two values are equal, and ===, which checks both that the values and the type of the values are equal, as shown in the next listing. Because JavaScript is weakly typed, an expression such as "42" == 42 evaluates to true. This is surprising, because "42" is text, whereas 42 is a number.

清单 1.15。弱类型
const a: any = "你好世界";
常量 b: 任何 = 42;

控制台日志(a == b);        1个

console.log("42" == b);     2个

console.log("42" === b);    3个
const a: any = "hello world";
const b: any = 42;

console.log(a == b);        1

console.log("42" == b);     2

console.log("42" === b);    3

  • 1 打印“false”,尽管允许将字符串与数字进行比较。
  • 1 Prints “false,” though comparing a string to a number is allowed.
  • 2 打印“真”;JavaScript 运行时将值隐式转换为相同类型。
  • 2 Prints “true”; the JavaScript run time implicitly converts the values to the same type.
  • 3 打印“假”;=== 运算符也比较类型。
  • 3 Prints “false”; the === operator also compares the types.

隐式类型转换很方便,因为我们不必编写更多代码来在类型之间进行显式转换,但它们很危险,因为在许多情况下我们不希望发生转换并且对结果感到惊讶。aTypeScript 是强类型的,当我们正确声明为 astringb为 a时,它不会编译前面的任何比较number,如以下清单所示。

Implicit type conversions are handy in that we don’t have to write more code to explicitly convert between types, but they are dangerous because in many cases we do not want conversions to happen and are surprised by the results. TypeScript, being strongly typed, doesn’t compile any of the preceding comparisons when we properly declare a to be a string and b to be a number, as the following listing shows.

清单 1.16。强类型
const a: string =c"你好世界";    1 
const b:数字= 42;               1个

控制台日志(a == b);                2个

                                    2 
console.log("42" == b);             2个

                                    2 
console.log("42" === b);            2个
const a: string =c"hello world";    1
const b: number = 42;               1

console.log(a == b);                2

                                    2
console.log("42" == b);             2

                                    2
console.log("42" === b);            2

  • 1 a 和 b 不再被声明为 any,所以它们得到类型检查。
  • 1 a and b are no longer declared as any, so they get type checked.
  • 2 所有三个比较都无法编译,因为 TypeScript 不允许比较不同的类型。
  • 2 All three comparisons fail to compile, as TypeScript doesn’t allow comparing different types.

现在所有的比较都会导致错误"This condition will always return 'false' since the types 'string' and 'number' have no overlap"。类型检查器确定我们正在尝试比较不同类型的值并拒绝该代码。

All the comparisons now cause the error "This condition will always return 'false' since the types 'string' and 'number' have no overlap". The type checker determines that we are trying to compare values of different types and rejects the code.

虽然弱类型系统在短期内更容易使用,因为它不会强制程序员在类型之间显式转换值,但它不会提供我们从更强类型系统获得的相同保证。本章中描述的大部分好处以及本书其余部分中采用的技术如果没有得到正确执行,就会失去效力。

Although a weak type system is easier to work with in the short term, as it doesn’t force programmers to explicitly convert values between types, it does not provide the same guarantees we get from a stronger type system. Most of the benefits described in this chapter and the techniques employed in the rest of this book lose their effectiveness if they are not properly enforced.

请注意,尽管类型系统是动态的(运行时类型检查)或静态的(编译时类型检查),但它的优势在于一个范围:它执行的隐式转换越多,它就越弱。大多数类型系统,即使是强大的类型系统,也确实为被认为安全的转换提供了一些有限的隐式转换。一个常见的例子是转换为boolean:if (a)在大多数语言中即使a是 anumber或引用类型也会编译。另一个例子是加宽转换,我们将在第 4 章中详细介绍。TypeScript 仅使用number类型来表示数值,但在某些语言中,例如,我们需要一个 16 位整数,但传入一个8 位整数,转换通常自动完成,因为没有数据损坏的风险。(16 位整数可以表示 8 位整数可以表示的任何值,甚至更多。)

Note that although a type system is either dynamic (type checking at run time) or static (type checking at compile time), its strength lies on a spectrum: the more implicit conversions it performs, the weaker it is. Most type systems, even strong ones, do provide some limited implicit casting for conversions that are deemed safe. A common example is conversions to boolean: if (a) in most languages would compile even if a is a number or a reference type. Another example is widening casts, which we’ll cover in detail in chapter 4. TypeScript uses only the number type to represent numeric values, but in languages in which, for example, we need a 16-bit integer but pass in an 8-bit integer, the conversion is usually done automatically, as there is no risk of data corruption. (A 16-bit integer can represent any value that an 8-bit integer can, and more.)

1.4.3.类型推断

1.4.3. Type inference

在某些情况下,编译器可以推断出变量或函数的类型,而无需我们明确指定。42例如,如果我们将值赋给一个变量,TypeScript 编译器可以推断出它的类型是number,因此我们不需要提供类型符号。如果我们想要明确并使代码的读者清楚类型,我们可以这样做,但符号并不是严格要求的。

In some cases, the compiler can infer the type of a variable or a function without us having to specify it explicitly. If we assign the value 42 to a variable, for example, the TypeScript compiler can infer that its type is number, so we don’t need to provide the type notations. We can do so if we want to be explicit and make the type clear to readers of the code, but the notation is not strictly required.

同样,如果一个函数在每条return语句中返回一个相同类型的值,我们不需要在函数定义中明确说明它的返回类型。编译器可以从代码中推断出它,如下一个清单所示。

Similarly, if a function returns a value of the same type on each return statement, we don’t need to spell out its return type explicitly in the function definition. The compiler can infer it from the code, as shown in the next listing.

清单 1.17。类型推断
函数添加(x:数字,y:数字){     1
    返回 x + y;
}

让 sum = add(40, 2);                   2个
function add(x: number, y: number) {    1
    return x + y;
}

let sum = add(40, 2);                   2

  • 1 该函数没有明确的返回类型,但编译器将其推断为数字。
  • 1 The function does not have an explicit return type, but the compiler infers it as number.
  • 2 变量sum的类型没有明确声明为number;相反,它是推断的。
  • 2 The type of the variable sum is not explicitly declared as number; rather, it is inferred.

与仅在运行时执行类型的动态类型不同,在这些情况下,类型仍然在编译时确定和检查,但我们不必显式提供它。如果键入不明确,编译器将发出错误并要求我们通过提供类型符号来更明确。

Unlike dynamic typing, in which typing is performed only at run time, in these cases the typing is still determined and checked at compile time, but we don’t have to supply it explicitly. If typing is ambiguous, the compiler will issue an error and ask us to be more explicit by providing type notations.

1.5. 在本书中

1.5. In this book

强大的静态类型系统使我们能够编写更正确、更可组合和更易读的代码。本书将涵盖此类现代类型系统的常见特征,重点是这些特征的实际应用。

A strong, static type system enables us to write code that is more correct, more composable, and more readable. This book will cover common features of such modern type systems with a focus on practical applications of these features.

我们将从原始类型开始,这是大多数语言中可用的现成类型。我们将介绍如何正确使用它们并避免一些常见的陷阱。在某些情况下,如果您的特定语言本身不提供这些类型,我们会展示如何实现其中一些类型。

We’ll start with primitive types, the out-of-the-box types available in most languages. We’ll cover using them correctly and avoiding some common pitfalls. In some cases, we show how to implement some of these types if your particular language does not provide them natively.

接下来,我们将研究组合以及如何将基本类型放在一起以构建支持您的特定问题域的大型类型。组合类型的方法有多种,因此您将了解如何根据要解决的特定问题为工作选择正确的工具。

Next, we’ll look at composition and how primitive types can be put together to build a large universe of types supporting your particular problem domain. There are multiple ways to combine types, so you’ll learn how to pick the right tool for the job depending on the particular problem you are trying to solve.

然后我们将介绍函数类型和当类型系统可以类型化函数并将它们视为常规值时向我们开放的新实现。函数式编程是一个非常深奥的话题,因此我们不会尝试全面解释它,而是借用一组有用的概念并将它们应用到非函数式语言中以解决现实世界中的问题。

Then we will cover function types and the new implementations that open to us when a type system can type functions and treat them as regular values. Functional programming is a very deep topic, so instead of attempting to explain it fully, we’ll borrow a set of useful concepts and apply them to a nonfunctional language to solve real-world problems.

在能够键入值、组合类型和类型函数之后,类型系统进化的下一步是子类型化。我们将回顾是什么使一种类型成为另一种类型的子类型,并了解如何将一些面向对象的编程概念应用到我们的代码中。我们将讨论继承、组合和不太传统的混合。

The next step in the evolution of type systems, after being able to type values, compose types, and type functions, is subtyping. We’ll go over what makes a type a subtype of another type and see how we can apply some object-oriented programming concepts to our code. We’ll discuss inheritance, composition, and the less-traditional mix-ins.

我们将继续使用泛型,它启用类型变量并允许我们对类型的代码进行参数化。泛型打开了一个全新的抽象和可组合性级别,将数据与数据结构、数据结构与算法解耦,并启用自适应算法。

We’ll continue with generics, which enable type variables and allow us to parameterize code on types. Generics open a whole new level of abstraction and composability, decoupling data from data structures, data structures from algorithms, and enabling adaptive algorithms.

最后,我们将介绍更高种类的类型,它们是下一个抽象级别,参数化泛型类型。更高种类的类型将数据结构形式化,例如幺半群和单子。今天许多编程语言不支持更高种类的类型,但它们在 Haskell 等语言中的广泛使用和日益流行最终将导致它们在更成熟的语言中被采用。

Last, we’ll cover higher kinded types, which are the next level of abstraction, parameterizing generic types. Higher kinded types formalize data structures such as monoids and monads. Many programming languages do not support higher kinded types today, but their extensive use in languages such as Haskell and increasing popularity will eventually lead to their adoption across more established languages.

概括

Summary

  • 类型是一种数据分类,它定义了可以对该数据执行的操作、数据的含义以及允许值的集合。
  • A type is a classification of data that defines the operations that can be done on that data, the meaning of the data, and the set of allowed values.
  • 类型系统是一组规则,用于为编程语言的元素分配和强制类型。
  • A type system is a set of rules that assigns and enforces types to elements of a programming language.
  • 类型限制了变量可以取值的范围,因此在某些情况下,运行时错误会变成编译时错误。
  • Types restrict the range of values a variable can take, so in some cases, what would’ve been a run-time error becomes a compile-time error.
  • 不变性是通过键入启用的数据属性,它确保值在不应该更改时不会更改。
  • Immutability is a property of the data enabled by typing, which ensures that values don’t change when they’re not supposed to.
  • 可见性是另一个类型级别的属性,它确定允许哪些组件访问哪些数据。
  • Visibility is another type-level property that determines which components are allowed to access which data.
  • 泛型编程支持强大的解耦和代码重用。
  • Generic programming enables powerful decoupling and code reuse.
  • 类型符号使代码的读者更容易理解代码。
  • Type notations make code easier to understand for readers of the code.
  • 动态类型(或鸭子类型)在运行时确定类型。
  • Dynamic typing (or duck typing) determines types at run time.
  • 静态类型在编译时检查类型,捕获否则会变成运行时错误的类型错误。
  • Static typing checks types at compile time, catching type errors that otherwise would’ve become run-time errors.
  • 类型系统的强度是衡量允许类型之间隐式转换的数量。
  • The strength of a type system is a measure of how many implicit conversions between types are allowed.
  • 现代类型检查器具有强大的类型推断算法,使它们能够确定变量、函数等的类型,而无需您明确地将它们写出来。
  • Modern type checkers have powerful type inference algorithms that enable them to determine the types of variables, functions, and so on without your having to write them out explicitly.

第 2 章中,我们将了解原始类型,它们是类型系统的构建块。我们将学习如何避免使用这些类型时出现的一些常见错误,并了解如何从数组和引用构建几乎任何数据结构。

In chapter 2, we will look at primitive types, which are the building blocks of the type system. We’ll learn how to avoid some common mistakes that arise when using these types and see how we can build almost any data structure from arrays and references.

第2章。基本类型

Chapter 2. Basic types

本章涵盖

This chapter covers

  • 常见的原始类型及其用途
  • Common primitive types and their uses
  • 如何评估布尔表达式
  • How Boolean expressions are evaluated
  • 数字类型和文本编码的缺陷
  • Pitfalls of numerical types and text encoding
  • 构建数据结构的基本类型
  • Fundamental types for building data structures

计算机在内部将数据表示为位序列。类型赋予这些序列以意义。同时,类型限制了任何数据可以取的可能值的范围。类型系统提供一组原始类型或内置类型以及一组用于组合这些类型的规则。

Computers represent data internally as sequences of bits. Types give meaning to these sequences. At the same time, types restrict the range of possible values any piece of data can take. Type systems provide a set of primitive or built-in types and a set of rules for combining these types.

在本章中,我们将了解一些常用的基本类型(空、单元、布尔值、数字、字符串、数组和引用)、它们的用途以及需要注意的常见陷阱。尽管我们每天都在使用基本类型,但每种类型都有细微差别,我们必须注意才能有效地使用它们。例如,布尔表达式可以短路,数值表达式可以溢出。

In this chapter we will look at some of the commonly available primitive types (empty, unit, Booleans, numbers, strings, arrays, and references), their uses, and common pitfalls to be aware of. Although we use primitive types every day, each comes with subtle nuances we must be aware of to use them effectively. Boolean expressions can be short-circuited, for example, and numerical expressions can overflow.

我们将从一些最简单的类型开始,这些类型携带很少或没有信息,然后转向通过各种编码表示数据的类型。最后,我们来看看数组和引用,它们是所有其他更复杂数据结构的构建块。

We’ll start with some of the simplest types, which carry little or no information, and move on to types that represent data via various encodings. Finally, we’ll look at arrays and references, which are building blocks for all other more-complex data structures.

2.1. 设计不返回值的函数

2.1. Designing functions that don’t return values

将类型视为可能值的集合,您可能想知道是否存在表示空集的类型。空集没有元素,所以这将是一个我们永远无法为其创建实例的类型。这样的类型有用吗?

Viewing types as sets of possible values, you may wonder whether there is a type to represent the empty set. The empty set has no elements, so this would be a type for which we can never create an instance. Would such a type be useful?

2.1.1.空类型

2.1.1. The empty type

作为实用程序库的一部分,让我们看看如何定义一个函数,该函数在给定消息的情况下记录发生错误的事实,包括时间戳和消息,然后抛出异常,如下一个清单所示。这样的函数是 的包装器throw,因此它并不意味着返回值。

As part of a utility library, let’s see how we would define a function that, given a message, logs the fact that an error occurred, including a timestamp and the message, and then throws an exception, as shown in the next listing. Such a function is a wrapper over throw, so it is not meant to return a value.

清单 2.1。如果找不到配置文件,则引发并记录错误
const fs = require("fs");

函数 raise(message: string): never {                            1
    console.error(`错误“${message}”在 ${new Date()} 处引发);
    抛出新的错误(消息);
}

函数 readConfig(configFile: string): string {
    if (!fs.existsSync(configFile))                                 2 
        raise(`配置文件 ${configFile} 丢失`);         3个

    返回 fs.readFileSync(configFile, "utf-8");
}
const fs = require("fs");

function raise(message: string): never {                           1
    console.error(`Error "${message}" raised at ${new Date()}`);
    throw new Error(message);
}

function readConfig(configFile: string): string {
    if (!fs.existsSync(configFile))                                2
        raise(`Configuration file ${configFile} missing`);         3

    return fs.readFileSync(configFile, "utf-8");
}

  • 1 函数从不返回(总是抛出),所以它的返回类型是never。
  • 1 The function never returns (always throws), so its return type is never.
  • 2 示例使用:如果找不到配置文件,我们要记录并抛出错误。
  • 2 Example use: if a config file is not found, we want to log and throw an error.

请注意,示例中函数的返回类型是never. 这让代码的读者清楚地知道raise()永远不会返回。更好的是,如果后来有人不小心更新了该函数并使其返回,则代码将不再编译。绝对不能给 赋值never,因此编译器确保该函数保持设计的行为并且永不返回。

Note that the return type of the function in the example is never. This makes it clear to readers of the code that raise() is never meant to return. Even better, if someone accidentally updates the function later and makes it return, the code no longer compiles. Absolutely no value can be assigned to never, so the compiler ensures that the function keeps behaving as designed and never returns.

这样的类型被命名为无法居住的类型空类型,因为无法创建它的实例。

Such a type is named an uninhabitable type or empty type because no instance of it can be created.

空类型

空类型是不能有任何值的类型:它的可能值集是空集。我们永远不能实例化这种类型的变量。我们使用空类型来表示不可能,例如将它用作永不返回(永远抛出或循环)的函数的返回类型。

An empty type is a type that cannot have any value: its set of possible values is the empty set. We can never instantiate a variable of such a type. We use an empty type to denote impossibility, such as by using it as the return type of a function that never returns (throws or loops forever).

不可居住的类型用于声明永不返回的函数。一个函数可能不会返回有几个原因:它可能在所有代码路径上抛出异常,它可能永远循环,或者它可能使程序崩溃。所有这些场景都是有效的。我们可能希望实现一个函数,在发生不可恢复的错误时抛出异常或崩溃之前执行一些日志记录或发送一些遥测数据。我们可以拥有希望在整个系统关闭之前不断循环运行的代码,例如系统的事件处理循环。

An uninhabitable type is used to declare a function that never returns. A function might not return for several reasons: it might throw an exception on all code paths, it might loop forever, or it might crash the program. All these scenarios are valid. We might want to implement a function that does some logging or sends some telemetry before throwing an exception or crashing in case of unrecoverable error. We can have code that we want to run continuously on a loop until the whole system is shut down, such as the event-processing loop of the system.

将这样的函数声明为 returning 是一种void误导,这是大多数编程语言用来指示缺少有意义值的类型。我们的函数不仅不返回有意义的值,而且根本不返回!

Declaring such a function as returning void, which is the type used by most programming languages to indicate the absence of a meaningful value, is misleading. Our function not only doesn’t return a meaningful value, but also doesn’t return at all!

非终止函数

空类型可能看起来微不足道,但它显示了数学和计算机科学之间的根本区别:在数学中,我们不能定义一个从非空集到空集的函数。这根本没有意义。数学中的函数不是“求值”的;他们只是“是”。

The empty type might seem trivial, but it shows a fundamental difference between mathematics and computer science: in mathematics, we cannot define a function from a nonempty set to an empty set. This simply doesn’t make sense. Functions in mathematics are not “evaluated”; they simply “are.”

另一方面,计算机评估程序;他们一步一步地执行指令。计算机最终可能会评估一个无限循环,这意味着它们永远不会停止执行。出于这个原因,计算机程序可以为空集定义一个有意义的函数,如前面的示例所示。

Computers, on the other hand, evaluate programs; they execute instructions step by step. Computers can end up evaluating an infinite loop, which means that they would never stop their execution. For this reason, a computer program can define a meaningful function to the empty set, as in the preceding examples.

每当你有一个非返回函数或者想明确表明它不可能有一个值时,考虑使用空类型。

Consider using an empty type whenever you have a nonreturning function or otherwise want to explicitly show that it’s impossible to have a value.

DIY空型

并非所有主流语言都像 TypeScript 那样提供内置的空类型never,但您可以在大多数语言中实现一个。您可以通过定义一个没有元素的枚举或一个只有私有构造函数的结构来实现这一点,这样它就永远不会被调用。

Not all mainstream languages provide a built-in empty type like never in TypeScript, but you can implement one in most of them. You can do this by defining an enumeration with no elements or a structure with only a private constructor such that it can never be called.

清单 2.2显示了我们如何在 TypeScript 中将一个空类型实现为一个无法实例化的类。请注意,如果两种类型具有相似的结构,TypeScript 认为它们是兼容的,因此我们需要添加一个虚拟void属性以确保其他代码不会以可以键入为Empty. 其他语言(例如 Java 和 C#)不需要此附加属性,因为它们不会认为类型基于形状是兼容的。我们将在第 7 章中更详细地介绍这一点。

Listing 2.2 shows how we would implement an empty type in TypeScript as a class that can’t be instantiated. Note that TypeScript considers two types to be compatible if they have similar structure, so we need to add a dummy void property to ensure that other code cannot end up with a value that can be typed as Empty. Other languages, such as Java and C#, would not need this additional property, as they wouldn’t consider types to be compatible based on shape. We’ll cover this in more detail in chapter 7.

清单 2.2。作为不可实例化类实现的空类型
声明 const EmptyType:唯一符号;                            1个

类空{
    [空类型]: void;                                             1
    私有构造函数() { }                                       3
}

function raise(message: string):{                            3
    console.error(`错误“${message}”在 ${new Date()} 处引发);
    抛出新的错误(消息);
}
declare const EmptyType: unique symbol;                            1

class Empty {
    [EmptyType]: void;                                             1
    private constructor() { }                                      3
}

function raise(message: string): Empty {                           3
    console.error(`Error "${message}" raised at ${new Date()}`);
    throw new Error(message);
}

  • 1 一种特定于 TypeScript 的方法,以确保具有相同形状的其他对象不会被解释为该类型
  • 1 A TypeScript-specific way to ensure that other objects with the same shape can’t be interpreted as this type
  • 2 私有构造函数确保其他代码无法实例化该类型
  • 2 Private constructor ensures that other code cannot instantiate this type
  • 3 这个函数和前面的例子一样,这次用的是Empty而不是never。
  • 3 This function is the same as in the previous example, this time using Empty instead of never.

代码编译,因为编译器执行控制流分析并确定不需要return语句。另一方面,添加return语句应该是不可能的,因为我们不能创建Empty.

The code compiles, as the compiler performs control flow analysis and determines no return statement is needed. On the other hand, it should be impossible to add a return statement, as we cannot create an instance of Empty.

2.1.2.单位类型

2.1.2. The unit type

在上一节中,我们研究了永不返回的函数。返回但不返回任何有用信息的函数呢?有很多这样的函数,我们调用它们只是为了它们的副作用:它们做了一些事情,改变了一些外部状态,但不执行任何有用的计算来返回给我们。

In the previous section, we looked at functions that never return. What about functions that do return but don’t return anything useful? There are many functions like this, which we call only for their side effects: they do something, change some external state, but don’t perform any useful computation to return to us.

让我们举console.log()个例子:它把它的参数输出到调试控制台,但不返回任何有意义的东西。另一方面,该函数在完成执行时确实会将控制权返回给调用者,因此它的返回类型不能是never.

Let’s take console.log() as an example: it outputs its argument to the debug console, but doesn’t return anything meaningful. On the other hand, the function does return control to the caller when it finishes executing, so its return type can’t be never.

"Hello world!"下一个清单中显示的经典函数是另一个很好的例子。我们称它为打印问候语(这是副作用),而不是返回值,因此我们将其返回值指定为void.

The classic "Hello world!" function shown in the next listing is another good example. We call it to print a greeting (which is a side effect), not to return a value, so we specify its return value as void.

清单 2.3。一个“你好世界!” 功能
功能问候():无效{             1
    console.log("你好世界!");
}

迎接();                            2个
function greet(): void {            1
    console.log("Hello world!");
}

greet();                            2

  • 1 该函数打印一条问候语,但不返回任何有用的信息。
  • 1 The function prints a greeting and doesn’t return anything useful.
  • 2 我们通常只是忽略此类函数的结果。
  • 2 We usually just ignore the result of such functions.

这种函数的返回类型称为unit type,一种只允许一个值的类型,它在 TypeScript 和大多数其他语言中的名称是void。我们通常没有类型变量void并且可以简单地从void函数返回而不提供实际值的原因是单元类型的值并不重要。

The return type of such a function is called a unit type, a type that allows just one value, and its name in TypeScript and most other languages is void. The reason why we usually don’t have variables of type void and can simply return from a void function without providing an actual value is that the value of a unit type is not important.

单位类型

单位类型是一种只有一个可能值的类型。如果我们有一个这种类型的变量,那么检查它的值就没有意义了;它只能是一个值。当函数的结果没有意义时,我们使用单位类型。

A unit type is a type that has only one possible value. If we have a variable of such a type, there is no point in checking its value; it can only be the one value. We use unit types when the result of a function is not meaningful.

接受任意数量的参数但不返回任何有意义的值的函数也称为动作(因为它们通常执行一个或多个改变世界状态的操作)或消费者(因为参数输入但没有输出)。

Functions that take any number of arguments but don’t return any meaningful value are also called actions (because they usually perform one or more operations that change the state of the world) or consumers (because arguments go in but nothing comes out).

DIY单元类型

尽管大多数编程语言都提供 like 类型void,但某些语言void以特殊方式处理,可能不允许您以与任何其他类型完全相同的方式使用它。在这种情况下,您可以通过定义具有单个元素的枚举或没有状态的单例来创建自己的单元类型。因为一个单位类型只有一个可能的值,所以那个值是什么并不重要;所有单位类型都是等价的。从一种单位类型转换为另一种单位类型很简单,因为没有选择:一种类型的单个值映射到另一种类型的单个值。

Although a type like void is available in most programming languages, some languages treat void in a special way and may not allow you to use it exactly the same way as any other type. In such situations, you can create your own unit type by defining an enumeration with a single element or a singleton without state. Because a unit type has only one possible value, it doesn’t really matter what that value is; all unit types are equivalent. It’s trivial to convert from one unit type to another, as there is no choice to be made: the single value of one type maps to the single value of the other one.

清单 2.4展示了我们如何在 TypeScript 中实现一个单元类型。至于 DIY 空类型,我们正在使用一个void属性来确保另一个具有兼容结构的类型不会被隐式转换为Unit. 其他语言(如 Java 和 C#)不需要此附加属性。

Listing 2.4 shows how we would implement a unit type in TypeScript. As for the DIY empty type, we are using a void property to ensure that another type with a compatible structure is not implicitly converted to Unit. Other languages, such as Java and C#, would not need this additional property.

清单 2.4。单元类型实现为无状态的单例
声明 const UnitType:唯一符号;

类单位{
    [单位类型]:无效;                            1 个
    静态只读值:Unit = new Unit();    2
    私有构造函数(){};                   3个
}

功能问候():单位{                          4
    console.log("你好世界!");
    返回单位值;                           4 
}
declare const UnitType: unique symbol;

class Unit {
    [UnitType]: void;                            1
    static readonly value: Unit = new Unit();    2
    private constructor() { };                   3
}

function greet(): Unit {                         4
    console.log("Hello world!");
    return Unit.value;                           4
}

  • 1 独特的符号属性确保具有相似形状的类型不会被解释为单位。
  • 1 Unique symbol property ensures that types with similar shape cannot be interpreted as Unit.
  • 2 Unit 类型的静态只读属性是 Unit 的唯一可能实例。
  • 2 Static read-only property of type Unit is the only possible instance of Unit.
  • 3 私有构造函数确保其他代码无法实例化该类型。
  • 3 Private constructor ensures that other code cannot instantiate this type.
  • 4 相当于一个返回void的函数,this总是返回完全相同的值。
  • 4 Equivalent to a function returning void, this always returns exactly the same value.

2.1.3.练习

2.1.3. Exercises

1个

set()接受一个值并将其分配给全局变量的函数 的返回类型应该是什么?

  1. never
  2. undefined
  3. void
  4. any

1

What should be the return type of a set() function that takes a value and assigns it to a global variable?

  1. never
  2. undefined
  3. void
  4. any

2个

terminate()立即停止程序执行的函数 的返回类型应该是什么?

  1. never
  2. undefined
  3. void
  4. any

2

What should be the return type of a terminate() function that immediately stops execution of the program?

  1. never
  2. undefined
  3. void
  4. any

2.2. 布尔逻辑和短路

2.2. Boolean logic and short circuits

在没有可能值的类型(空类型,例如never)和具有一个可能值的类型(单元类型,例如void)之后,是具有两个可能值的类型。在大多数编程语言中可用的规范二值类型是布尔类型。

After types with no possible values (empty types such as never) and types with one possible value (unit types such as void), come types with two possible values. The canonical two-valued type, available in most programming languages, is the Boolean type.

布尔值编码真实性。这个名字来自 George Boole,他介绍了现在所谓的布尔代数,一种由真值 (1) 和假值 (0) 以及对它们的逻辑运算(例如 、 和 )组成ANDOR代数NOT

Boolean values encode truthiness. The name comes from George Boole, who introduced what is now called Boolean algebra, an algebra consisting of truth (1) and falseness (0) values and logical operations on them such as AND, OR, and NOT.

一些类型系统提供布尔值作为内置类型,其值truefalse. 其他系统依赖于数字,认为 0 代表意义false,任何其他数字代表意义true(也就是说,任何不是 false 的都是 true)。booleanTypeScript 有一个带有可能值的内置类型truefalse.

Some type systems provide Booleans as a built-in type with values true and false. Other systems rely on numbers, considering 0 to mean false and any other number to mean true (that is, whatever is not false is true). TypeScript has a built-in boolean type with possible values true and false.

无论原始布尔类型是否存在,或者真实值是从其他类型的值中推断出来的,大多数编程语言都使用某种形式的布尔语义来启用条件分支if (condition) { ... }只有当条件的计算结果为真时,诸如 的语句才会执行大括号之间的部分。循环依赖于条件来确定是迭代还是完成:while (condition) { ... }。没有条件分支,我们将无法编写非常有用的代码。想想你将如何实现一个非常简单的算法,例如在数字列表中找到第一个偶数,而不需要任何循环或条件语句。

Regardless of whether a primitive Boolean type exists or truthiness values are inferred from values of other types, most programming languages use some form of Boolean semantics to enable conditional branching. A statement such as if (condition) { ... } will execute the part between curly brackets only if the condition evaluates to something true. Loops rely on conditions to determine whether to iterate or finish: while (condition) { ... }. Without conditional branching, we wouldn’t be able to write very useful code. Think about how you would implement a very simple algorithm, such as finding the first even number in a list of numbers, without any loops or conditional statements.

2.2.1.布尔表达式

2.2.1. Boolean expressions

许多编程语言对常见的布尔运算使用以下符号:&&for AND||forOR!for NOT。布尔表达式通常用真值表来描述(图 2.1)。

Many programming languages use the following symbols for common Boolean operations: && for AND, || for OR, and ! for NOT. Boolean expressions are usually described with truth tables (figure 2.1).

图 2.1。AND, OR, 和NOT真值表

2.2.2.短路评估

2.2.2. Short circuit evaluation

假设您必须为评论系统构建一个网守,如清单 2.5所示:当用户尝试发表评论时,网守拒绝彼此相隔 10 秒内发布的评论(用户在发送垃圾邮件)和内容为空的评论(用户不小心在键入任何内容之前单击评论)。

Suppose that you must build a gatekeeper for a commenting system as shown in listing 2.5: as users attempt to post comments, the gatekeeper rejects comments posted within 10 seconds of each other (the user is spamming) and comments with empty contents (the user accidentally clicked Comment before typing anything).

看门人函数将评论和用户 ID 作为参数。您secondsSinceLastComment()已经实现了一个功能;此函数在给定用户 ID 的情况下查询数据库并返回自上次发布以来的秒数。

The gatekeeper function takes as arguments the comment and the user ID. You have a secondsSinceLastComment() function already implemented; this function, given the user ID, queries the database and returns the number of seconds since the last post.

如果两个条件都满足,则将评论发布到数据库;如果不是,返回false

If both conditions are met, post the comment to the database; if not, return false.

清单 2.5。看门人
声明函数 secondsSinceLastComment(userId: string): number;       1
声明函数 postComment(comment: string, userId: string): void;    2个

function commentGatekeeper(comment: string, userId: string): 布尔值{
    如果((secondsSinceLastComment(userId)<10)||(评论==“”))       3
        返回假;

    postComment(评论, userId);

    返回真;
}
declare function secondsSinceLastComment(userId: string): number;       1
declare function postComment(comment: string, userId: string): void;    2

function commentGatekeeper(comment: string, userId: string): boolean {
    if ((secondsSinceLastComment(userId) < 10) || (comment == ""))      3
        return false;

    postComment(comment, userId);

    return true;
}

  • 1 secondsSinceLastComment 在数据库中查询用户上次发帖的年龄。
  • 1 secondsSinceLastComment queries the database for the age of the user’s last post.
  • 2 postComment 将评论写入数据库。
  • 2 postComment writes the comment to the database.
  • 3 如果不满足其中一个条件,则返回 false。否则,发表评论并返回 true。
  • 3 If one of the conditions isn’t met, return false. Otherwise, post comment and return true.

清单 2.5是网守的一个可能实现。请注意如果最后一条评论的年龄(以秒为单位)小于 10当前评论为空, OR我们返回的表达式。false

Listing 2.5 is a possible implementation of the gatekeeper. Note the OR expression where we return false if either the age of the last comment in seconds is less than 10 or the current comment is empty.

实现相同逻辑的另一种方法是切换两个操作数,如以下清单所示。首先检查当前评论是否为空;然后检查最后发表评论的年龄,如清单 2.5所示。

Another way to implement the same logic is to switch the two operands, as shown in the following listing. First check whether the current comment is empty; then check the age of the last posted comment, as in listing 2.5.

清单 2.6。替代网守实施
声明函数 secondsSinceLastComment(userId: string): number;
声明函数 postComment(comment: string, userId: string): void;

function commentGatekeeper(comment: string, userId: string): 布尔值{
    if ( (comment == "") || (secondsSinceLastComment(userId) < 10) )        1
        返回假;

    postComment(评论, userId);

    返回真;
}
declare function secondsSinceLastComment(userId: string): number;
declare function postComment(comment: string, userId: string): void;

function commentGatekeeper(comment: string, userId: string): boolean {
    if ((comment == "") || (secondsSinceLastComment(userId) < 10))       1
        return false;

    postComment(comment, userId);

    return true;
}

  • 1 此版本与上一版本的唯一区别是翻转条件。
  • 1 The only difference between this version and the previous one is the flipped conditions.

一个版本在任何方面都比另一个更好吗?它们定义了相同的检查——只是顺序不同。事实证明,它们是不同的。根据收到的输入,它们在运行时的行为因布尔表达式的计算方式而异。

Is one version better in any way than the other? They define the same checks—just in a different order. As it turns out, they are different. Depending on the input received, they behave differently at run time due to the way Boolean expressions are evaluated.

大多数编译器和运行时为布尔表达式执行称为短路的优化。形式的表达a AND b被翻译成if a then b else false. 这符合真值表AND:如果第一个操作数为假,则无论第二个操作数是什么,整个表达式都是假的。另一方面,如果第一个操作数为真,则如果第二个操作数也为真,则整个表达式为真。

Most compilers and run times perform an optimization called short circuit for Boolean expressions. Expressions of the form a AND b are translated to if a then b else false. This respects the truth table for AND: if the first operand is false, then regardless of what the second operand is, the whole expression is false. On the other hand, if the first operand is true, then the whole expression is true if the second operand is also true.

类似的翻译发生在a OR b,变成if a then true else b。查看 的真值表OR,如果第一个操作数为真,则无论第二个操作数是什么,整个表达式都为真;否则,如果第一个操作数为假,则如果第二个操作数为真,则表达式为真。

A similar translation happens for a OR b, which becomes if a then true else b. Looking at the truth table for OR, if the first operand is true, then the whole expression is true regardless of what the second operand is; otherwise, if the first operand is false, then the expression is true if the second operand is true.

这种翻译和名称短路的原因来自这样一个事实,即如果评估第一个操作数提供了足够的信息来评估整个表达式,则根本不会评估第二个操作数。看门人功能必须执行两项检查:一项相对便宜的检查,以确保它收到的评论不为空;另一项可能代价高昂的检查,涉及查询评论数据库。在清单 2.5中,数据库查询首先发生。如果最近发表的评论超过 10 秒,短路甚至不会查看当前评论,只会返回false。在清单 2.6中,如果当前评论为空,则不会查询数据库。第二个版本可能会通过评估便宜的检查来跳过昂贵的检查。

The reason for this translation and the name short circuit come from the fact that if evaluating the first operand provides enough information to evaluate the whole expression, the second operand is not evaluated at all. The gatekeeper function must perform two checks: a relatively inexpensive one, to make sure that the comment it receives is not empty, and a potentially expensive one, which involves querying the comment database. In listing 2.5, the database query happens first. If the last posted comment is more recent than 10 seconds, short-circuiting will not even look at the current comment and will simply return false. In listing 2.6, if the current comment is empty, the database doesn’t get queried. The second version can potentially skip an expensive check by evaluating a cheap check.

布尔表达式求值的这个属性很重要,在组合条件时要记住:短路可以跳过右侧表达式的求值,这取决于左侧表达式的求值结果,因此优先选择从最便宜到最贵的。

This property of Boolean expression evaluation is important and something to remember when you are combining conditions: short-circuiting can skip evaluation of the expression on the right, depending on the result of evaluating the expression on the left, so prefer ordering conditions from cheapest to most expensive.

2.2.3.锻炼

2.2.3. Exercise

1个

以下代码将打印什么?

让计数器:数字= 0;

函数条件(值:布尔值):布尔值{
    计数器++;
    返回值;
}

如果(条件(假)&&条件(真)){
    // ...
}

控制台日志(计数器)

  1. 0
  2. 1个
  3. 2个
  4. 没有什么; 它抛出一个错误。

1

What will the following code print?

let counter: number = 0;

function condition(value: boolean): boolean {
    counter++;
    return value;
}

if (condition(false) && condition(true)) {
    // ...
}

console.log(counter)

  1. 0
  2. 1
  3. 2
  4. Nothing; it throws an error.

2.3. 数值类型的常见陷阱

2.3. Common pitfalls of numerical types

在大多数编程语言中,数字通常作为一种或多种原始类型提供。在处理数字时,您应该注意几个陷阱。举个例子,一个简单的函数可以计算购物总额。如果一个用户以每支 10 美分的价格购买三支泡泡糖,我们预计总计为 30 美分。根据我们使用数字类型的方式,我们可能会感到惊讶。

Numbers are usually provided as one or more primitive types in most programming languages. There are several gotchas you should be aware of when working with numbers. Take, for example, a simple function that adds up a shopping total. If a user purchases three sticks of bubble gum at 10 cents each, we would expect the total to be 30 cents. Depending on how we use numerical types, we might be in for a surprise.

清单 2.7。函数加总项目
type Item = { name: string, price: number };          1个

函数 getTotal(items: Item[]): number {             2
    让总计:数字= 0;

    对于(让项目的项目){
        总计 += item.price;
    }

    返回总计;
}

让总计:数字= getTotal(
    [{ 名称:“樱桃泡泡糖”,价格:0.10 },        3 
     { 名称:“薄荷泡泡糖”,价格:0.10 },          3 
     { 名称:“草莓泡泡糖”,价格:0.10 }]    3
);

控制台日志(总计 == 0.30);                           4个
type Item = { name: string, price: number };          1

function getTotal(items: Item[]): number {            2
    let total: number = 0;

    for (let item of items) {
        total += item.price;
    }

    return total;
}

let total: number = getTotal(
    [{ name: "Cherry bubblegum", price: 0.10 },       3
     { name: "Mint bubblegum", price: 0.10 },         3
     { name: "Strawberry bubblegum", price: 0.10 }]   3
);

console.log(total == 0.30);                           4

  • 1 我们用名称和价格(数字)表示商品。
  • 1 We represent an item by a name and a price (number).
  • 2 getTotal 函数返回一个数字作为总数。
  • 2 The getTotal function returns a number as the total.
  • 3 计算三支泡泡糖的总数,每支 10 美分。
  • 3 Compute total for three sticks of bubble gum, 10 cents each.
  • 4 这会打印“false”,即使我们期望 0.10 + 0.10 + 0.10 为 0.30。
  • 4 This prints “false,” even though we would expect 0.10 + 0.10 + 0.10 to be 0.30.

为什么 0.10 三次相加得不到 0.30?要理解这一点,我们需要看看计算机是如何表示数字类型的。数字类型的两个定义特征是它的宽度和它的编码。

Why does adding up 0.10 three times not give us 0.30? To understand this, we need to look at how numerical types are represented by computers. The two defining characteristics of a numerical type are its width and its encoding.

宽度是用来表示一个值的位数。这可以从 8 位(一个字节)甚至 1 位到 64 位或更多。位宽与底层芯片架构有很大关系:64 位 CPU 具有 64 位寄存器,因此可以对 64 位值进行极快的操作。对给定宽度的数字进行编码有三种常用方法:unsigned binarytwo's complementIEEE 754

The width is the number of bits used to represent a value. This can range from 8 bits (a byte) or even 1 bit up to 64 bits or more. Bit widths have a lot to do with the underlying chip architecture: a 64-bit CPU has 64-bit registers, thus allowing extremely fast operations on 64-bit values. There are three common ways to encode numbers of a given width: unsigned binary, two’s complement, and IEEE 754.

2.3.1.整数类型和溢出

2.3.1. Integer types and overflow

无符号二进制编码使用每一位来表示部分值。例如,一个 4 位无符号整数可以表示从0到 的任何值15。通常,一个N位无符号整数可以表示从0(所有位都是0)到(所有位都是)的值。图 2.2显示了 4 位无符号整数的几个可能值。您可以使用公式 b N –1 * 2 N–1 + b N–2 * 2 N将N个二进制数字序列( b N –1 b N–2 ...b 1 b 0 ) 转换为十进制数–2 + ... + b 12N-11* 2 1 + b 0 * 2 0

An unsigned binary encoding uses every bit to represent part of the value. A 4-bit unsigned integer, for example, can represent any value from 0 to 15. In general, an N-bit unsigned integer can represent values from 0 (all bits are 0) up to 2N-1 (all bits are 1). Figure 2.2 shows a few possible values of a 4-bit unsigned integer. You can convert a sequence of N binary digits (bN–1bN–2...b1b0) to a decimal number with the formula bN–1 * 2N–1 + bN–2 * 2N–2 + ... + b1 * 21 + b0 * 20.

图 2.2。4 位无符号整数编码。当所有 4 位都是 时,最小的可能值00。当所有位均为 时,最大可能值为1( 151 * 2 3 + 1 * 2 2 + 1 * 2 1 + 1 * 2 0 )。

这种编码非常简单,但只能表示正数。如果我们还想表示负数,我们需要不同的编码,通常是二进制补码。在二进制补码编码中,我们保留一个位来对符号进行编码。正数与以前完全相同,而负数通过从 2 N中减去它们的绝对值来编码,其中N是位数。图 2.3显示了 4 位有符号整数的几个可能值。

This encoding is very straightforward but can represent only positive numbers. If we also want to represent negative numbers, we need a different encoding, which is usually two’s complement. In two’s complement encoding, we reserve a bit to encode the sign. Positive numbers are represented exactly as before, whereas negative numbers are encoded by subtracting their absolute value from 2N, where N is the number of bits. Figure 2.3 shows a few possible values of a 4-bit signed integer.

图 2.3。4 位有符号整数编码。–8被编码为 2 4 – 8(二进制 1000),–3被编码为 2 4 – 3(二进制 1101)。第一位始终1用于负数和0正数。

使用这种编码,所有负数都有第一位1,所有正数和 0 都有第一位0。一个 4 位有符号整数可以表示从–8到 的值7。我们用来表示一个值的位数越多,我们可以表示的值范围就越大。

With this encoding, all negative numbers have the first bit 1, and all positive numbers and 0 have the first bit 0. A 4-bit signed integer can represent values from –8 to 7. The more bits we use to represent a value, the larger the value range we can represent.

上溢和下溢

但是,当算术运算的结果不能用给定的位数表示时会发生什么?如果我们使用 4 位无符号编码并尝试添加 10 + 10 怎么办,即使我们可以用 4 位表示的最大值是15

What happens, though, when the result of an arithmetic operation can’t be represented within the given number of bits? What if we are using a 4-bit unsigned encoding and try to add 10 + 10, even though the maximum value we can represent in 4 bits is 15?

这种情况称为算术溢出。相反的情况,即我们最终得到的数字太小而无法表示,称为算术下溢。不同的语言以不同的方式处理这些情况(图 2.4)。

Such a situation is called an arithmetic overflow. The opposite situation, in which we end up with a number that is too small to represent, is called an arithmetic underflow. Different languages treat these situations in different ways (figure 2.4).

处理算术上溢和下溢的三种主要方法是回绕、饱和或错误输出。

The three main ways to handle arithmetic overflow and underflow are to wrap around, saturate, or error out.

图 2.4。处理算术溢出的不同方法。里程表从 999999 回绕到 0;一个旋钮简单地停在可能的最大值;袖珍计算器打印Error并停止。

环绕是硬件通常所做的,因为它只是丢弃不适合的位。对于一个 4 位无符号整数,如果位是1111并且我们加 1,结果是10000,但是因为只允许 4 位,一个被丢弃,我们最终得到0000,回到0。这是处理溢出的最有效方法,但也是最危险的方法,因为它可能导致意外结果。将 1 美元加到我的 15 美元上,我最终可以得到 0 美元。

Wrap around is what the hardware usually does, as it simply discards the bits that don’t fit. For a 4-bit unsigned integer, if the bits are 1111 and we add 1, the result is 10000, but because only 4 bits are allowed, one gets discarded, and we end up with 0000, wrapping back around to 0. This is the most efficient way to handle overflow but also the most dangerous, as it can cause unexpected results. Adding $1 to my $15, I can end up with $0.

饱和度是另一种处理溢出的方法。如果一个操作的结果超过了最大可表示值,我们就简单地停在最大值处。这很好地映射到物理世界:如果你的恒温器只上升到某个温度,试图让它变暖并不会改变它。另一方面,使用饱和,算术运算不再总是关联的。如果7是我们的最大值,则 7 + (2 – 2) = 7 + 0 = 7 但 (7 + 2) – 2 = 7 – 2 = 5。

Saturation is another way to handle overflow. If the result of an operation exceeds the maximum representable value, we simply stop at the maximum. This maps well to the physical world: if your thermostat only goes up to some temperature, trying to make it warmer won’t change that. On the other hand, using saturation, arithmetic operations are no longer always associative. If 7 is our maximum value, 7 + (2 – 2) = 7 + 0 = 7 but (7 + 2) – 2 = 7 – 2 = 5.

第三种可能性error out是在发生溢出时抛出错误。这是最安全的方法,但缺点是需要检查每一个算术运算,并且无论何时执行任何算术,您的代码都需要处理异常情况。

The third possibility, error out, is to throw an error when an overflow happens. This is the safest approach but has the drawback that every single arithmetic operation needs to be checked, and whenever you perform any arithmetic, your code needs to handle exceptional cases.

检测上溢和下溢

根据您使用的语言,算术上溢和下溢可以通过这些方式中的任何一种来处理。如果您的方案需要与语言默认值不同的处理,您需要检查操作是否会溢出或下溢并单独处理该方案。诀窍是在允许值的范围内执行此操作。

Depending on the language you are using, arithmetic overflows and underflows could be handled in any one of these ways. If your scenario requires different handling from the language default, you need to check whether an operation would overflow or underflow and handle that scenario separately. The trick is to do this within the range of allowed values.

例如,要检查添加值是否ab溢出或下溢 [ MIN, MAX] 范围,我们需要确保我们没有a + b < MIN(添加两个负数时)或a + b > MAX

To check whether adding values a and b would overflow or underflow a [MIN, MAX] range, for example, we need to ensure that we don’t have a + b < MIN (when adding two negative numbers) or a + b > MAX.

如果b是正数,我们不可能有a + b < MIN,因为我们正在变得a更大,而不是更小。在这种情况下,我们只需要检查溢出。我们可以重写a + b > MAXa > MAX – bb两边相减)。因为我们减去一个正数,所以我们正在使值变小,所以不存在溢出的风险(MAX – b在范围内[MIN, MAX])。所以我们溢出 ifb > 0a > MAX – b

If b is positive, we can’t possibly have a + b < MIN, as we’re making a bigger, not smaller. In this case, we only need to check for overflow. We can rewrite a + b > MAX as a > MAX – b (subtract b on both sides). Because we’re subtracting a positive number, we are making the value smaller, so there is no risk of overflowing (MAX – b is within the [MIN, MAX] range). So we overflow if b > 0 and a > MAX – b.

如果b是负数,我们不可能有a + b > MAX,因为我们正在变a小,而不是变大。在这种情况下,我们只需要检查下溢。我们可以重写a + b < MIN as a < MIN – bb两边相减)。因为我们减去一个负数,所以我们正在使值变大,所以不存在下溢的风险(MIN – b[MIN, MAX]范围内)。所以我们下溢 ifb < 0a < MIN – b,如下一个清单所示。

If b is negative, we can’t possibly have a + b > MAX, as we’re making a smaller, not bigger. In this case, we only need to check for underflow. We can rewrite a + b < MIN as a < MIN – b (subtract b on both sides). Because we’re subtracting a negative number, we are making the value larger, so there is no risk of underflowing (MIN – b is within the [MIN, MAX] range). So we underflow if b < 0 and a < MIN – b, as shown in the next listing.

清单 2.8。检查加法溢出
函数 addError(a: 数字, b: 数字,
    最小值:数字,最大值:数字):布尔值 {     1
    如果 (b >= 0) {
        返回一个 > 最大 - b;                 2个
    } 别的 {
        返回 a < min - b;                 3个
    }
}
function addError(a: number, b: number,
    min: number, max: number): boolean {    1
    if (b >= 0) {
        return a > max - b;                 2
    } else {
        return a < min - b;                 3
    }
}

  • 1 该函数采用数字 a 和 b,以及允许的最小值和最大值。
  • 1 The function takes the numbers a and b, and the minimum and maximum allowed values.
  • 2 如果 b 为正,则如果 a > max – b,则会发生溢出。
  • 2 If b is positive, we have an overflow if a > max – b.
  • 3 如果 b 为负,则如果 a < min – b,则出现下溢。
  • 3 If b is negative, we have an underflow if a < min – b.

我们可以使用类似的逻辑进行减法。

We can use similar logic for subtraction.

对于乘法,我们通过在两边除以 来检查上溢和下溢b。在这里,我们需要考虑两个数字的符号,因为两个负数相乘得到一个正数,而一个正数和一个负数相乘得到一个负数。

For multiplication, we check for overflow and underflow by dividing on both sides by b. Here, we need to consider the signs of both numbers, as multiplying two negative numbers yields a positive number, whereas multiplying a positive and a negative number yields a negative number.

如果我们溢出

We overflow if

  • b > 0,a > 0a > MAX / b
  • b > 0, a > 0, and a > MAX / b
  • b < 0,a < 0a < MAX / b
  • b < 0, a < 0, and a < MAX / b

我们下溢如果

We underflow if

  • b > 0,a < 0a < MIN / b
  • b > 0, a < 0, and a < MIN / b
  • b < 0,a > 0a > MIN / b
  • b < 0, a > 0, and a > MIN / b

对于整数除法, 的值a / b始终是一个整数,其值介于 -a和 之间a[-a,a]如果不完全在 内,我们只需要检查上溢和下溢[MIN,MAX]。回到我们的 4 位有符号整数示例,其中MINis 8 和MAXis 7,除法溢出的唯一情况是 8 / 1(因为 [ 8,8] 不完全在 [ 8,7] 内) . 事实上,对于有符号整数,唯一的溢出情况是什么时候a是最小可表示值并且b 1。无符号整数除法永远不会溢出。

For integer division, the value of a / b is always an integer whose value is between - a and a. We only need to check for overflow and underflow if [-a,a] is not fully within [MIN,MAX]. Going back to our 4-bit signed integer example, where MIN is 8 and MAX is 7, the only case where division overflows is 8 / 1 (because [8,8] is not fully within [8,7]). In fact, for signed integers, the only overflow scenario is when a is the minimum representable value and b is 1. Unsigned integer division can never overflow.

表 2.12.2总结了在需要特殊处理时检查上溢和下溢的必要步骤。

Tables 2.1 and 2.2 summarize the steps necessary to check for overflow and underflow when special handling is required.

表 2.1。在 MIN = –MAX-1 的 [MIN, MAX] 范围内检测 a 和 b 的整数溢出

添加

Addition

减法

Subtraction

乘法

Multiplication

分配

Division

b > 0 且 a > MAX – b b < 0 和 a > MAX + b b > 0,a > 0,且 a > MAX / b b < 0,a < 0,且 a < MAX / b a == MIN 和 b == -1
表 2.2。在 MIN = –MAX-1 的 [MIN, MAX] 范围内检测 a 和 b 的整数下溢

添加

Addition

减法

Subtraction

乘法

Multiplication

分配

Division

b < 0 且 a < 最小值 – b b > 0 且 a < 最小值 + b b > 0,a < 0,且 a < MIN / b b < 0,a > 0,且 a > MIN / b 不适用

2.3.2.浮点类型和舍入

2.3.2. Floating-point types and rounding

IEEE 754 是电气和电子工程师协会的标准,用于表示浮点数或带小数部分的数字。在 TypeScript(和 JavaScript)中,数字使用binary64编码表示为 64 位浮点数。图 2.5详细说明了这种表示。

IEEE 754 is the Institute of Electrical and Electronics Engineers standard for representing floating-point numbers, or numbers with a fractional part. In TypeScript (and JavaScript), numbers are represented as 64-bit floating-point using the binary64 encoding. Figure 2.5 details this representation.

图 2.5。0.10 的浮点表示。首先,我们看到三个组成部分在内存中的二进制表示:符号位、指数和尾数。下面,我们有将二进制表示形式转换为数字的公式。最后,我们看到应用公式的结果:0.10 近似于 0.100000000000000005551115123126。

浮点数的三个组成部分是符号、指数和尾数。符号是用于正数或负数的单个位。尾数是一个分数如图 2.2中的公式所示。这个分数乘以 2 提高到偏置指数01

The three components of a floating-point number are the sign, the exponent, and the mantissa. The sign is a single bit that is 0 for positive numbers or 1 for negative numbers. The mantissa is a fraction as described by the formula in figure 2.2. This fraction is multiplied by 2 raised to the biased exponent.

指数之所以称为有偏差,是因为我们从指数表示的无符号整数中减去一个值,使其既可以表示正数也可以表示负数。在 binary64 的情况下,该值为 1023。IEEE 754 标准定义了几种编码,其中一些使用基数 10 而不是基数 2,尽管基数 2 在实践中出现得更多。

The exponent is called biased because from the unsigned integer represented by the exponent, we subtract a value so that it can represent both positive and negative numbers. In the binary64 case, the value is 1023. The IEEE 754 standard defines several encodings, some using base 10 instead of base 2, though base 2 appears more often in practice.

该标准还定义了特殊值:

The standard also defines special values:

  • NaN, 表示不是数字,用于表示无效运算的结果,例如除以0。
  • NaN, which stands for not a number and is used to represent the result of invalid operations, such as division by 0.
  • 正负无穷大(Inf),运算溢出时作为饱和值使用
  • Positive and negative infinity (Inf), which are used when operations overflow as saturation values
  • 即使0.10根据公式变成0.100000000000000005551115123126,也被四舍五入为0.1。事实上,0.10 和 0.100000000000000005551115123126 在 JavaScript 中是相等的。浮点数可以使用相对较少的位数表示很大范围内的小数的唯一方法是舍入和近似。
  • Even though 0.10 becomes 0.100000000000000005551115123126 according to the formula, it is rounded down to 0.1. In fact, 0.10 and 0.100000000000000005551115123126 compare as equal in JavaScript. The only way floating-point can represent fractional numbers across a huge range of values using a relatively small number of bits is by rounding and approximating.
精度值

如果需要精度——例如在处理货币时——避免使用浮点数。将 0.10 相加三次不等于 0.30 的原因是,虽然每个单独的 0.10 表示都四舍五入为 0.10,但将它们相加会产生一个四舍五入为 0.30000000000000004 的数字。

If precision is needed—in dealing with currency, for example—avoid using floating-point numbers. The reason why adding 0.10 together three times doesn’t equal 0.30 is that although each individual 0.10 representation gets rounded to 0.10, adding them yields a number that rounds to 0.30000000000000004.

小整数可以安全地表示而无需四舍五入,因此将价格编码为一对美元和美分整数是一个更好的主意。JavaScript 提供Number.isSafeInteger(),它告诉我们是否可以在不舍入的情况下表示整数值,因此依靠它,我们可以设计一个Currency类型来编码两个整数值并防止舍入问题,如下一个清单所示。

Small integer numbers can safely be represented without rounding, so it is a better idea to encode a price as a pair of dollars and cents integers. JavaScript provides Number.isSafeInteger(), which tells us whether an integer value can be represented without rounding, so relying on that, we can design a Currency type that encodes two integer values and protects against rounding issues, as the next listing shows.

清单 2.9。币种及币种添加功能
类货币{
    私人美元:数量;                                           1
    私仙:数;                                             1个

    构造函数(美元:数字,美分:数字){
        如果(!Number.isSafeInteger(美元))                             2
            throw new Error("不能安全地表示美元金额");

        如果 (!Number.isSafeInteger(cents))                               2
            throw new Error("不能安全地表示美分金额");

        this.dollars = 美元;
        this.cents = 美分;
    }

    getDollars(): 数字 {                                              3
        返回 this.dollars;
    }

    getCents(): 数字 {                                                3
        返回this.cents;
    }
}

函数添加(货币 1:货币,货币 2:货币):货币 {
    返回新货币(
        currency1.getDollars() + currency2.getDollars(),                4 
        currency1.getCents() + currency2.getCents());                  4 
}
class Currency {
    private dollars: number;                                           1
    private cents: number;                                             1

    constructor(dollars: number, cents: number) {
        if (!Number.isSafeInteger(dollars))                            2
            throw new Error("Cannot safely represent dollar amount");

        if (!Number.isSafeInteger(cents))                              2
            throw new Error("Cannot safely represent cents amount");

        this.dollars = dollars;
        this.cents = cents;
    }

    getDollars(): number {                                             3
        return this.dollars;
    }

    getCents(): number {                                               3
        return this.cents;
    }
}

function add(currency1: Currency, currency2: Currency): Currency {
    return new Currency(
        currency1.getDollars() + currency2.getDollars(),               4
        currency1.getCents() + currency2.getCents());                  4
}

  • 1 我们将美元和美分金额存储在单独的变量中。
  • 1 We store dollars and cents amounts in separate variables.
  • 2 构造函数确保我们只存储无需舍入即可安全表示的值。
  • 2 Constructor ensures that we store only values that can be safely represented without rounding.
  • 3 金额通过 getter 访问,因此外部代码无法修改它们。
  • 3 The amounts are accessed via getters, so external code cannot modify them.
  • 4 添加两个货币值只是添加美元和美分的金额。
  • 4 Adding two Currency values simply adds the dollar and cents amounts.

在另一种语言中,我们会使用两种整数类型并防止上溢/下溢。因为 JavaScript 不提供整型原始类型,我们依赖于Number.isSafeInteger()防止舍入。在处理货币时,与其让货币神秘地出现或消失,不如出错。

In another language we would’ve used two integer types and protected against overflow/underflow. Because JavaScript does not provide an integer primitive type, we rely on Number.isSafeInteger() to protect against rounding. When dealing with currency, it’s better to error out than to have money mysteriously appear or disappear.

清单 2.9中的类仍然很简单。一个很好的练习是扩展它,以便每 100 美分自动转换为 1 美元。您必须注意在何处检查安全整数:如果美元金额是一个安全整数,但将其加 1(从 100 美分开始)会使它不安全怎么办?

The class in listing 2.9 is still pretty bare-bones. A good exercise is to extend it so that every 100 cents gets automatically converted to a dollar. You must be careful about where to check for safe integers: what if the dollar amount is a safe integer but adding 1 to it (from 100 cents) makes it unsafe?

比较浮点数

正如我们所见,由于四舍五入,比较浮点数是否相等通常不是一个好主意。有一种更好的方法可以判断两个值是否大致相同:我们可以确保它们的差异在给定的阈值内。

As we’ve seen, because of rounding, it’s usually not a good idea to compare floating-point numbers for equality. There is a better way to tell whether two values are approximately the same: we can make sure that their difference is within a given threshold.

这个阈值应该是多少?它应该是最大可能的舍入误差。该值称为机器 epsilon,并且是特定于编码的。JavaScript 将此值作为Number.EPSILON. 使用这个值,我们可以实现两个数字之间的相等比较,取它们差的绝对值并检查它是否小于机器 epsilon。如果是,则这些值在彼此的舍入误差范围内,因此我们可以认为它们相等。

What should this threshold be? It should be the maximum possible rounding error. This value is called a machine epsilon and is encoding-specific. JavaScript provides this value as Number.EPSILON. Using this value, we can implement an equality comparison between two numbers, taking the absolute value of their difference and checking whether it is smaller than the machine epsilon. If it is, the values are within rounding error of each other, so we can consider them equal.

清单 2.10。epsilon 内的浮点相等性
函数 epsilonEqual(a:数字,b:数字):布尔值 {
    返回 Math.abs(a - b) <= Number.EPSILON;           1个
}

控制台日志(0.1 + 0.1 + 0.1 == 0.3);                    2 
console.log(epsilonEqual(0.1 + 0.1 + 0.1, 0.3));        3个
function epsilonEqual(a: number, b: number): boolean {
    return Math.abs(a - b) <= Number.EPSILON;           1
}

console.log(0.1 + 0.1 + 0.1 == 0.3);                    2
console.log(epsilonEqual(0.1 + 0.1 + 0.1, 0.3));        3

  • 1 检查两个数字是否在彼此的舍入误差范围内。
  • 1 Check whether the two numbers are within rounding error of each other.
  • 2 打印“false”,因为 0.1 + 0.1 + 0.1 舍入为 0.30000000000000004。
  • 2 Prints “false” because 0.1 + 0.1 + 0.1 rounds to 0.30000000000000004.
  • 3 打印“true”,因为 0.3 和 0.30000000000000004 在彼此的舍入误差范围内。
  • 3 Prints “true” because 0.3 and 0.30000000000000004 are within rounding error of each other.

一般来说,epsilonEqual()在比较两个浮点数时使用类似的东西是个好主意,因为算术运算会导致舍入错误,从而导致意外结果。

It’s a good idea in general to use something like epsilonEqual() whenever comparing two floating-point numbers, as arithmetic operations can cause rounding errors that lead to unexpected results.

2.3.3.任意大的数字

2.3.3. Arbitrarily large numbers

大多数语言都有提供任意大数字的库。这些类型将它们的宽度扩展到表示任何值所需的尽可能多的位。Python 提供了这样一种类型作为默认的数值类型,BigInt目前提出了一种任意大的类型用于 JavaScript 的标准化。也就是说,我们不会将任意大的数字视为原始类型,因为它们可以由固定宽度的数字类型构建。它们很方便,但许多运行时本身并不提供它们,因为没有等效的硬件。(芯片总是在固定数量的位上运行。)

Most languages have libraries that provide arbitrarily large numbers. These types extend their width to as many bits as needed to represent any value. Python provides such a type as the default numerical type, and an arbitrarily large BigInt type is currently proposed for standardization for JavaScript. That being said, we won’t treat arbitrarily large numbers as primitive types because they can be built out of fixed-width numerical types. They are convenient, but many run times do not provide them natively, as there is no hardware equivalent. (Chips always operate on a fixed number of bits.)

2.3.4.练习

2.3.4. Exercises

1个

以下代码将打印什么?

让一个:数字= 0.3;
让 b: 数字 = 0.9;

控制台日志(a * 3 == b);

  1. 没有什么; 它抛出一个错误。
  2. true
  3. false
  4. 0.9

1

What will the following code print?

let a: number = 0.3;
let b: number = 0.9;

console.log(a * 3 == b);

  1. Nothing; it throws an error.
  2. true
  3. false
  4. 0.9

2个

跟踪唯一标识符的数字的溢出行为应该是什么?

  1. 溢出时饱和。
  2. 溢出时环绕。
  3. 溢出时出错。
  4. 他们中的任何一个都可以。

2

What should be the overflow behavior of a number that tracks unique identifiers?

  1. Saturate on overflow.
  2. Wrap around on overflow.
  3. Error on overflow.
  4. Any of them is OK.

2.4. 编码文本

2.4. Encoding text

另一种常见的原始类型是string,它用于表示文本。字符串由零个或多个字符组成,这使它成为我们涵盖的第一个可以具有无限值集的原始类型。

Another common primitive type is the string, which is used to represent text. A string consists of zero or more characters, which makes it the first primitive type we are covering that can have an infinite set of values.

在早期的计算机中,每个字符都由一个字节表示,因此计算机最多有 256 个字符可用于表示文本。随着 Unicode 的标准化,旨在提供一种表示世界上所有字母表和其他字符(例如表情符号)的方法,256 个字符显然是不够的。事实上,Unicode 定义了超过一百万个字符!

In the early days of computers, each character was represented by a single byte, so computers had at most 256 characters available to represent text. With the standardization of Unicode, which aims to provide a way to represent all the world’s alphabets and other characters (such as emojis), 256 characters obviously are not enough. In fact, Unicode defines more than one million characters!

2.4.1.中断文本

2.4.1. Breaking text

让我们以一个简单的文本断开函数为例,它接受一个字符串并将其拆分为多个给定长度的字符串,以便它可以适应文本编辑器控件的宽度,如以下代码所示。

Let’s take as an example a simple text-breaking function that takes a string and splits it into multiple strings of a given length so that it can fit within the width of a text-editor control, as shown in the following code.

清单 2.11。简单的文本拆分功能
函数 lineBreak(text: string, lineLength: number): string[] {
    让行:string[] = [];                                      1个

    while (text.length > lineLength) {                              2 
        lines.push(text.substr(0, lineLength));                    3 
        text = text.substr(lineLength);                            3个
    }

    lines.push(文本);                                              4个
    返回线;
}
function lineBreak(text: string, lineLength: number): string[] {
    let lines: string[] = [];                                      1

    while (text.length > lineLength) {                             2
        lines.push(text.substr(0, lineLength));                    3
        text = text.substr(lineLength);                            3
    }

    lines.push(text);                                              4
    return lines;
}

  • 1 lines 数组将包含拆分文本。
  • 1 The lines array will contain the split text.
  • 2 只要文本的长度大于一行的长度,就重复。
  • 2 Repeat as long as the length of the text is larger than the length of a line.
  • 3 添加文本的第一个lineLength个字符作为新行;然后将它们从文本中删除。
  • 3 Add the first lineLength characters of text as a new line; then chop them from the text.
  • 4 将剩余文本(小于 lineLength)作为最后一行添加到结果中。
  • 4 Add the remaining text (smaller than lineLength) to the result as the final line.

乍一看,这个实现似乎是正确的。对于输入文本,例如"Testing, testing"行长为5,结果行为["Testi", "ng, t", "estin", "g"]。这是我们所期望的,因为文本每五个字符被分成多行。

At first look, this implementation seems to be correct. For input text such as "Testing, testing" and a line length of 5, the resulting lines are ["Testi", "ng, t", "estin", "g"]. This is what we expect, as the text is divided into multiple lines at every fifth character.

不过,其他符号具有更复杂的编码。举个例子,女警表情符号“ ”。尽管这看起来像一个字符,但 Java-Script 用五个字符来表示它。 回报。如果我们尝试根据它在文本中出现的位置来分解包含此表情符号的字符串,我们可能会得到意想不到的结果。如果我们尝试将文本“ ”拆分为行长,我们将取回数组。 "".length5...5["...", ""]

Other symbols have more complex encodings, though. Take, for example, “”, the woman police-officer emoji. Even though this looks like a single character, Java-Script represents it with five characters. "".length returns 5. If we try to break a string containing this emoji, depending on where it appears in the text, we can get unexpected results. If we try to break the text “...” with a line length of 5, we get back the array ["...", ""].

女警官表情符号由两个独立的表情符号组成:警官表情符号和女性标志表情符号。这两个表情符号与零宽度连接字符组合在一起"\ud002"。该字符没有图形表示;相反,它用于组合其他字符。

The woman police-officer emoji is composed of two separate emojis: the police-officer emoji and the female-sign emoji. The two emojis are combined with the zero-width joined character "\ud002". This character does not have a graphical representation; rather, it is used for combining other characters.

警察表情符号“ ”由两个相邻的字符表示,如果我们尝试将较长的字符串“ ”拆分为行长,我们可以观察到这一点。这最终分裂了警官表情符号,给了我们。是表示无法按原样打印的字符的 Unicode 转义序列。女警官表情符号,即使它被呈现为单个符号,也由五个不同的转义序列、和表示。 ....5["....\ud83d", "\udc6e"]\uXXXX\ud83d\udc6e, \u200d, \u2640\ufe0e

The police-officer emoji, “”, is represented with two adjacent characters, as we can observe if we try to split the longer string “....” with a line length of 5. This ends up splitting the police-officer emoji, giving us ["....\ud83d", "\udc6e"]. \uXXXX are Unicode escape sequences that represent a character that cannot be printed as is. The woman police-officer emoji, even though it gets rendered as a single symbol, is represented by the five distinct escape sequences \ud83d, \udc6e, \u200d, \u2640, and \ufe0e.

在字符边界处天真地打断文本会产生无法呈现的结果,甚至会改变文本的含义。

Naïvely breaking text at character boundaries can give results that can’t be rendered and can even change the meaning of the text.

2.4.2.编码

2.4.2. Encodings

我们需要查看字符编码以更好地理解如何正确处理文本。Unicode 标准使用两个相似但不同的概念:字符和字形。字符是文本的计算机表示(警官表情符号、零宽度连接符和女性符号),而字素是用户看到的符号(女警官)。渲染文本时,我们使用字素,我们不想分解多字符字素。在编码文本时,我们使用字符。

We need to look at character encodings to better understand how to handle text properly. The Unicode standard works with two similar but distinct concepts: characters and graphemes. Characters are the computer representations of text (police-officer emoji, zero-width joiner, and female sign), and graphemes are the symbols users see (woman police officer). When rendering text, we work with graphemes, and we don’t want to break apart a multiple-character grapheme. When encoding text, we work with characters.

字形和字素

字形是字符的特定表示。“ C ”(粗体)和“ C ”(斜体)是字符“C”的两种不同的视觉呈现。

A glyph is a particular representation of a character. “C” (bold) and “C” (italic) are two different visual renderings of the character “C”.

是一个不可分割的单位,如果将其拆分成多个部分,它将失去其意义,例如女警官的例子。一个字素可以由各种字形表示。女警察的 Apple 表情符号看起来与 Microsoft 的表情符号不同;它们是呈现相同字素的不同字形(图 2.6)。

A grapheme is an indivisible unit, which would lose its meaning if it were split into components, such as the woman police-officer example. A grapheme can be represented by various glyphs. The Apple emoji for woman police officer looks different from the Microsoft one; they are different glyphs rendering the same grapheme (figure 2.6).

图 2.6。女警官表情符号的字符编码(警官表情符号字符 + 零宽度连接符 + 女性符号表情符号)和生成的字素(女警官)。

每个 Unicode 字符都定义为一个代码点。0x0这是一个介于和 之间的值0x10FFFF,因此有 1,114,111 个可能的代码点。这些代码点代表了世界上所有的字母表、表情符号和许多其他符号,并有足够的空间供将来添加。

Each Unicode character is defined as a code point. This is a value between 0x0 and 0x10FFFF, so there are 1,114,111 possible code points. These code points represent all the world’s alphabets, emojis, and many other symbols, with plenty of room for future additions.

UTF-32

对这些代码点进行编码的最直接方法是 UTF-32,每个字符使用 32 位。一个 32 位整数可以表示介于0x0和 之间的值

The most straightforward way of encoding these code points is UTF-32, which uses 32 bits for each character. A 32-bit integer can represent values between 0x0 and

0xFFFFFFFF,因此它可以容纳任何有余地的代码点。UTF-32 的问题在于它非常低效,因为它浪费了大量未使用位的空间。因此,开发了几种更紧凑的编码,这些编码对较小的代码点使用较少的位,而随着值变大,使用更多的位。这些也称为可变长度 编码

0xFFFFFFFF, so it can fit any code point with room to spare. The problem with UTF-32 is that it’s very inefficient, as it wastes a lot of space with unused bits. Because of that, several more compact encodings were developed that use fewer bits for smaller code points and more bits as the values get larger. These are also called variable-length encodings.

UTF-16 和 UTF-8

最常用的编码是 UTF-16 和 UTF-8。UTF-16 是 JavaScript 使用的编码。在 UTF-16 中,单位是 16 位。适合 16 位(从0x00xFFFF)的代码点用单个 16 位整数表示,而需要超过 16 位(从0x100000x10FFFF)的代码点由两个 16 位值表示。

The most commonly used encodings are UTF-16 and UTF-8. UTF-16 is the encoding used by JavaScript. In UTF-16, the unit is 16 bits. Code points that fit in 16 bits (from 0x0 to 0xFFFF) are represented with a single 16-bit integer, whereas code points that require more than 16 bits (from 0x10000 to 0x10FFFF) are represented by two 16-bit values.

最流行的编码 UTF-8 将这种方法更进一步:单位是 8 位,代码点由一个、两个、三个或四个 8 位值表示。

UTF-8, the most popular encoding, takes this approach a step further: the unit is 8 bits and code points are represented by one, two, three, or four 8-bit values.

2.4.3.编码库

2.4.3. Encoding libraries

文本编码和操作是一个复杂的话题,整本书都专门讨论它。好消息是你不需要学习所有的细节来有效地使用字符串,但是你需要意识到它的复杂性并寻找机会来代替天真的文本操作,就像我们的文本中断示例中那样,调用封装这种复杂性的库。

Text encoding and manipulation is a complex topic, with whole books dedicated to it. The good news is that you don’t need to learn all the details to effectively work with strings, but you do need to be aware of the complexity and look for opportunities to replace naïve text manipulation, as in our text-breaking example, with calls to libraries that encapsulate this complexity.

grapheme-splitter,例如,是一个 JavaScript 文本库,可同时处理字符和字素。您可以通过运行来安装它npm install grapheme-splitter。使用grapheme-splitter,我们可以lineBreak()通过将文本拆分为字素数组然后将它们分组为字素字符串来实现在lineLength字素级别拆分文本的功能,如以下清单所示。

grapheme-splitter, for example, is a JavaScript text library that works with both characters and graphemes. You can install it by running npm install grapheme-splitter. With grapheme-splitter, we can implement the lineBreak() function to break the text at grapheme level by splitting the text into an array of graphemes and then grouping them in strings of lineLength graphemes, as the following listing shows.

清单 2.12。使用 grapheme-splitter 库的文本断开功能
导入 GraphemeSplitter = require("grapheme-splitter");
const splitter = new GraphemeSplitter();

函数 lineBreak(文本:字符串,lineLength:数字){
    let graphemes: string[] = splitter.splitGraphemes(text);       1个
    让行:string[] = [];

    for (let i = 0; i < graphemes.length; i += lineLength) {        2 
        lines.push(graphemes.slice(i, i + lineLength).join(""));   2个
    }

    返回线;
}
import GraphemeSplitter = require("grapheme-splitter");
const splitter = new GraphemeSplitter();

function lineBreak(text: string, lineLength: number) {
    let graphemes: string[] = splitter.splitGraphemes(text);       1
    let lines: string[] = [];

    for (let i = 0; i < graphemes.length; i += lineLength) {       2
        lines.push(graphemes.slice(i, i + lineLength).join(""));   2
    }

    return lines;
}

  • 1 splitGraphemes 函数将字符串拆分为一个字素数组。
  • 1 The splitGraphemes function splits a string into an array of graphemes.
  • 2 然后我们得到 lineLength 字素的切片并将它们连接成文本行。
  • 2 We then get slices of lineLength graphemes and join them into lines of text.

通过此实现,行长度为.......5.....[".....", ""].

With this implementation, the strings “...” and “....” for a line length of 5 do not split the string at all, as none of the strings is larger than five graphemes, and the string “.....” correctly gets split into [".....", ""].

grapheme-splitter库有助于防止处理字符串时出现的三种常见错误之一:

The grapheme-splitter library helps prevent one of the three common classes of errors in dealing with strings:

  • 在字符级别而不是字素级别处理编码文本——这个例子在第 2.4.1 节中有介绍,我们在字符级别打断文本,即使出于渲染目的我们想在字素级别打断它。在第五个字符处断开可以将一个字素拆分成多个字素。在显示文本时,我们还需要了解字符序列如何组合成字素。
  • Manipulating encoded text at character level instead of grapheme level—This example was covered in section 2.4.1, where we broke text at character level, even though for rendering purposes we wanted to break it at the grapheme level. Breaking at the fifth character can split a grapheme into multiple graphemes. When displaying text, we also need to be aware of how sequences of characters combine into graphemes.
  • 在字节级别而不是字符级别操作编码文本——当我们在不知道编码的情况下错误地处理了一系列可变长度编码文本时,就会发生这种情况,在这种情况下,我们可能会将一个字符拆分为多个字符,例如,打破在第五个字节,即使我们打算在第五个字符处中断。根据实际字符的编码,它可能占用一个或多个字节,因此我们不应该做出任何忽略编码的假设。
  • Manipulating encoded text at byte level instead of character level—This situation happens when we incorrectly handle a sequence of variable-length encoded text without being aware of the encoding, in which case we might split a character into multiple characters by, for example, breaking at the fifth byte even though we meant to break at the fifth character. Depending on the encoding of the actual character, it might take up one or more bytes, so we shouldn’t make any assumptions that ignore encoding.
  • 将字节序列解释为编码错误的文本(例如尝试将 UTF-16 编码文本解释为 UTF-8 编码,反之亦然)——当从另一个组件接收文本作为字节序列时,您必须知道什么编码文本使用。不同的语言对文本有不同的默认编码,因此简单地将字节序列解释为字符串可能会给您错误的解释。
  • Interpreting a sequence of bytes as text with the wrong encoding (such as trying to interpret UTF-16 encoded text as UTF-8 encoded, or vice-versa)—When receiving text from another component as a sequence of bytes, you must know what encoding the text uses. Different languages have different default encodings for text, so simply interpreting byte sequences as strings may give you wrong interpretations.

图 2.7显示了女警官字素是如何由两个 Unicode 字符组成的。该图还显示了它们的 UTF-16 编码和二进制表示。

Figure 2.7 shows how the woman police-officer grapheme is composed out of two Unicode characters. The figure also shows their UTF-16 encoding and binary representation.

图 2.7。女警官表情符号被视为内存中位的 UTF-16 字符串编码、UTF-16 字节序列、Unicode 代码点序列和字素。

请注意,对于相同的字素,UTF-8 编码即使最终在屏幕上具有相同的表示形式,也是不同的。UTF-8 编码是0xF0 0x9F 0x91 0xAE 0xE2 0x80 0x8D 0xE2 0x99 0x80 0xEF 0xB8 0x8F.

Note that for the same grapheme, the UTF-8 encoding, even though it ends up having the same representation on screen, is different. The UTF-8 encoding is 0xF0 0x9F 0x91 0xAE 0xE2 0x80 0x8D 0xE2 0x99 0x80 0xEF 0xB8 0x8F.

始终确保您使用正确的编码解释字节序列,并依靠字符串库在字符和字素级别操作字符串。

Always make sure you are interpreting byte sequences with the right encoding, and rely on string libraries to manipulate strings at character and grapheme levels.

2.4.4.练习

2.4.4. Exercises

1个

编码一个 UTF-8 字符需要多少字节?

  1. 1字节
  2. 2个字节
  3. 4字节
  4. 这取决于性格。

1

How many bytes are needed to encode a UTF-8 character?

  1. 1 byte
  2. 2 bytes
  3. 4 bytes
  4. It depends on the character.

2个

编码一个 UTF-32 字符需要多少字节?

  1. 1字节
  2. 2个字节
  3. 4字节
  4. 这取决于性格。

2

How many bytes are needed to encode a UTF-32 character?

  1. 1 byte
  2. 2 bytes
  3. 4 bytes
  4. It depends on the character.

2.5. 使用数组和引用构建数据结构

2.5. Building data structures with arrays and references

我们将讨论的最后两种常见基本类型是数组和引用。有了这些,我们就可以构建任何其他更高级的数据结构,例如列表和树。这两个原语在实现数据结构时提供了不同的权衡。我们将探索如何根据预期的访问模式(读取与写入频率)和数据密度(稀疏与密集)最好地利用它们。

The last two common primitive types we will discuss are arrays and references. With these, we can build up any of the other more advanced data structures, such as lists and trees. These two primitives offer different trade-offs in implementing data structures. We’ll explore how to best leverage them depending on expected access patterns (read versus write frequency) and data density (sparse versus dense).

固定大小的数组依次存储给定类型的多个值,从而实现高效访问。引用类型允许我们通过让组件引用其他组件来将数据结构拆分到多个位置。

Fixed-size arrays store several values of a given type one after the other, enabling efficient access. Reference types allow us to split a data structure across multiple locations by having components reference other components.

我们不会将可变大小数组视为原始类型,因为它们是使用固定大小数组和/或引用实现的,正如我们将在本节中看到的那样。

We will not consider variable-size arrays to be primitive types, because these are implemented with fixed-size arrays and/or references, as we’ll see in this section.

2.5.1.固定大小的数组

2.5.1. Fixed-size arrays

固定大小的数组表示连续的内存范围,其中包含多个相同类型的值。例如,一个由五个 32 位整数组成的数组是 160 位 (5 * 32) 的范围,其中前 32 位存储第一个数字,后 32 位存储下一个数字,依此类推。

Fixed-size arrays represent a contiguous range of memory that contains several values of the same type. An array of five 32-bit integers, for example, is a range of 160 bits (5 * 32) in which the first 32 bits store the first number, the second 32 bits store the next, and so on.

数组之所以是一种常见的原语而不是链表,原因在于效率:因为值是一个接一个地存储的,所以访问其中任何一个都是一项快速操作。如果一个 32 位整数数组从内存地址 101 开始,这相当于说第一个整数(在索引 0 处)存储为 101 和 132 之间的 32 位,则数组中索引 N 处的整数位于101 + N * 32 通常,如果列表从地址base开始,并且元素的大小为M ,则可以在base + N * M找到索引N处的元素. 因为内存是连续的,所以数组很有可能被分页到内存中并立即缓存,从而实现非常快速的访问。

The reason why arrays are a common primitive as opposed to, say, linked lists is efficiency: because the values are stored one after the other, accessing any one of them is a fast operation. If an array of 32-bit integers starts at memory address 101, which is the same as saying that the first integer (at index 0) is stored as the 32 bits between 101 and 132, the integer at index N in the array is at 101 + N * 32. In general, if the list starts at address base, and the size of an element is M, the element at index N can be found at base + N * M. Because the memory is contiguous, there is a high chance the array will get paged into memory and cached all at once, which enables very fast access.

相比之下,对于链表,访问第Nth 个元素需要我们从链表的头部开始,沿着next每个节点的指针,直到我们到达N第一个。无法直接计算节点的地址。节点不一定一个接一个地分配,因此内存可能必须调入和调出,直到我们到达我们想要的节点。图 2.8显示了数组和整数链表在内存中的表示。

By contrast, for a linked list, accessing the Nth element requires us to start from the head of the list and follow the next pointers of each node until we reach the Nth one. There is no way to compute the address of a node directly. Nodes are not necessarily allocated one after the other, so memory might have to be paged in and out until we reach the node we want. Figure 2.8 shows in-memory representations of an array and a linked-list of integers.

图 2.8。五个 32 位整数存储在一个固定大小的数组和一个链表中。在固定大小的数组中查找元素非常快,因为我们可以计算出它的确切位置。另一方面,链表要求我们跟随元素next直到找到我们要找的元素。元素可以在内存中的任何位置。

术语“固定大小”源于数组不能就地增长或收缩的事实。如果我们想让我们的数组存储六个整数而不是五个,我们将不得不分配一个可以容纳六个整数的新数组并从原始数组复制前五个。将此与链表进行对比,我们可以在链表中添加一个节点而无需修改任何现有节点。根据预期的访问模式(更多读取或更多追加),一种表示会比另一种表现更好。

The term fixed-size comes from the fact that arrays can’t be grown or shrunk in place. If we ever want to make our array store six integers instead of five, we would have to allocate a new array that can fit six integers and copy the first five over from the original array. Contrast this with a linked list, in which we can append a node without having to modify any of the existing nodes. Depending on the expected access pattern (more reads or more appends), one representation would work better than the other.

2.5.2.参考

2.5.2. References

引用类型保存指向对象的指针。引用类型的值——变量的实际位——并不代表对象的内容,而是对象所在的位置。对单个对象的多个引用不会复制对象的状态,因此通过其中一个引用对对象所做的更改通过所有其他引用可见。

Reference types hold pointers to objects. The value of a reference type—the actual bits of a variable—do not represent the content of an object, but where the object can be found. Multiple references to a single object do not duplicate the state of the object, so changes made to the object through one of the references are visible through all other references.

引用类型通常用于数据结构实现中,因为它们提供了一种连接单独组件的方法,这些组件可以在运行时添加到数据结构或从数据结构中删除。

Reference types are commonly used in data structure implementations, as they provide a way to connect separate components that can be added to or removed from the data structure at run time.

在接下来的部分中,我们将了解一些常见的数据结构,以及如何使用数组、引用或两者的组合来实现它们。

In the following sections, we will look at a few common data structures and how they can be implemented with arrays, references, or a combination of the two.

2.5.3.高效列表

2.5.3. Efficient lists

许多语言都提供列表数据结构作为其库的一部分。注意这个数据结构不是原语,而是用原语实现的数据结构。随着项目的添加或删除,列表可以收缩和增长。

Many languages provide a list data structure as part of their library. Note this data structure is not a primitive, but a data structure implemented with primitives. Lists can shrink and grow as items are added or removed.

如果将列表实现为链表,我们可以添加和删除节点而无需复制任何数据,但遍历列表会很昂贵(线性时间或O(n)复杂性,其中n是列表的长度)。在清单 2.13中,NumberLinkedList是这样一个列表实现,它提供了两个函数:at(),它检索给定索引处的值,以及append(),它向列表的末尾添加一个值。该实现保留了两个引用:一个指向列表的开头,我们可以从中开始遍历,另一个指向列表的末尾,这样我们就可以在不必遍历列表的情况下追加元素。

If lists were implemented as linked lists, we could add and remove nodes without having to copy any data, but traversing the list would be expensive (linear time or O(n) complexity, where n is the length of the list). In listing 2.13, NumberLinkedList is such a list implementation that provides two functions: at(), which retrieves the value at the given index, and append(), which adds a value to the end of the list. The implementation keeps two references: one to the beginning of the list, from which we can start a traversal, and one to the end of the list, which allows us to append elements without having to traverse the list.

清单 2.13。链表实现
类 NumberListNode {
    值:数字;                                                   1
    下一个:NumberListNode | 不明确的;                                1个

    构造函数(值:数字){
        this.value = 值;
        this.next = undefined;
    }
}

类 NumberLinkedList {
    private tail: NumberListNode = { value: 0, next: undefined };    2
    私有头:NumberListNode = this.tail;                        2个

    在(索引:数字):数字{
        让结果:NumberListNode | undefined = this.head.next;     3 
        while (index > 0 && result != undefined) {                    3
            结果=结果.下一个;
            指数 - ;
        }

           如果(结果 == 未定义)抛出新的 RangeError();

        返回结果。值;
    }

    附加(值:数字){
        this.tail.next = { value: value, next: undefined };          4 
        this.tail = this.tail.next;                                  4个
    }
}
class NumberListNode {
    value: number;                                                   1
    next: NumberListNode | undefined;                                1

    constructor(value: number) {
        this.value = value;
        this.next = undefined;
    }
}

class NumberLinkedList {
    private tail: NumberListNode = { value: 0, next: undefined };    2
    private head: NumberListNode = this.tail;                        2

    at(index: number): number {
        let result: NumberListNode | undefined = this.head.next;     3
        while (index > 0 && result != undefined) {                   3
            result = result.next;
            index--;
        }

           if (result == undefined) throw new RangeError();

        return result.value;
    }

    append(value: number) {
        this.tail.next = { value: value, next: undefined };          4
        this.tail = this.tail.next;                                  4
    }
}

  • 1 列表中的节点具有值和对下一个节点的引用,或者如果这是最后一个节点则未定义。
  • 1 A node in the list has a value and a reference to the next node or is undefined if this is the last node.
  • 2 列表开始时是空的,头部和尾部都指向一个虚拟节点。
  • 2 The list starts as empty, with both head and tail pointing to a dummy node.
  • 3 要获取给定索引处的节点,我们必须从头开始并跟随下一个引用。
  • 3 To get the node at a given index, we must start from the head and follow the next references.
  • 4 添加一个节点是高效的:我们只需将它添加到尾部,然后更新尾部属性。
  • 4 Appending a node is efficient: we just add it to the tail and then update the tail property.

正如我们所见,append()在这种情况下非常有效,因为它只需要将一个节点添加到尾部,然后使该新节点成为尾部。另一方面,at()要求我们从头部开始,沿着next引用移动,直到到达我们要寻找的节点。

As we can see, append() is very efficient in this case, as it only needs to add a node to the tail and then make that new node the tail. On the other hand, at() requires us to start from the head and move along next references until we reach the node we were looking for.

在下一个清单中,让我们将其与基于数组的实现进行对比,在该实现中可以高效地访问元素,但附加元素是昂贵的操作。

In the next listing, let’s contrast this with an array-based implementation, in which accessing an element can be done efficiently, but appending an element is the expensive operation.

清单 2.14。基于数组的列表实现
类 NumberArrayList {
    私人号码:number[] = [];                               1
    私有长度:number = 0;                                   1个

    在(索引:数字):数字{
        if (index >= this.length) throw new RangeError();
        返回 this.numbers[index];                               2个
    }

    附加(值:数字){
        让 newNumbers: number[] = new Array(this.length + 1);    3 
        for (let i = 0; i < this.length; i++) {                    3
            newNumbers[i] = this.numbers[i];
        }
        newNumbers[this.length] = 值;                          4个
        this.numbers = newNumbers;
        这个。长度++;
    }
}
class NumberArrayList {
    private numbers: number[] = [];                               1
    private length: number = 0;                                   1

    at(index: number): number {
        if (index >= this.length) throw new RangeError();
        return this.numbers[index];                               2
    }

    append(value: number) {
        let newNumbers: number[] = new Array(this.length + 1);    3
        for (let i = 0; i < this.length; i++) {                   3
            newNumbers[i] = this.numbers[i];
        }
        newNumbers[this.length] = value;                          4
        this.numbers = newNumbers;
        this.length++;
    }
}

  • 1 我们将值存储在一个数字数组中,最初的长度为 0。
  • 1 We store the values in a number array, originally of 0 length.
  • 2 访问一个元素只是意味着在数组中建立索引。
  • 2 Accessing an element simply means indexing in the array.
  • 3 添加一个数字需要我们分配一个新数组并复制旧元素。
  • 3 Appending a number requires us to allocate a new array and copy the old elements.
  • 4 最后,将最后一个元素添加到新数组的末尾。
  • 4 Finally, the last element is added to the end of the new array.

在这里,访问给定索引处的元素仅意味着在基础numbers数组中进行索引。另一方面,附加一个值成为一个复杂的操作:

Here, accessing the element at a given index simply means indexing in the underlying numbers array. On the other hand, appending a value becomes an involved operation:

  1. 我们必须分配一个比当前数组大一个元素的新数组。
  2. We must allocate a new array one element larger than the current array.
  3. 然后我们必须将当前数组中的所有元素复制到新分配的数组中。
  4. Then we must copy over all the elements from the current array to the newly allocated one.
  5. 我们将该值附加为新数组中的最后一个元素。
  6. We append the value as the last element in the new array.
  7. 我们用新数组替换当前数组。
  8. We replace the current array with the new one.

每当我们需要追加一个新值时复制数组的所有元素,同样不是很有效。

Copying all the elements of the array whenever we need to append a new value is, again, not very efficient.

实际上,大多数库将列表实现为具有额外容量的数组。该数组的大小比最初需要的大,因此无需创建新数组和复制数据即可添加新元素。当数组被填满时,一个新数组被分配,元素被复制,但新数组的容量增加了一倍(图 2.9)。

In practice, most libraries implement lists as an array with extra capacity. The array has a larger size than initially needed, so new elements can be appended without having to create a new array and copy data. When the array gets filled up, a new array is allocated, and elements do get copied over, but the new array has double the capacity (figure 2.9).

图 2.9。一个基于数组的列表,包含 9 个元素,容量为 16。在必须将数据移动到更大的数组之前,可以附加七个元素。

使用这种试探法,数组容量呈指数级增长,因此数据不会像数组每次只增长一个元素那样被复制那么多。

With this heuristic, the array capacity grows exponentially, so data doesn’t get copied as much as it would if the array grew by only one element every time.

清单 2.15。具有额外容量的基于数组的列表实现
类 NumberList {
    私人号码:number[] = new Array(1);                    1个
    私有长度:number = 0;
    私有容量:number = 1;                               1个

    在(索引:数字):数字{
        if (index >= this.length) throw new RangeError();
        返回 this.numbers[index];                             2个
    }

    附加(值:数字){
        如果 (this.length < this.capacity) {                       3
             this.numbers[length] = value;                        3
            这个长度++;                                      3
            返回;
        }

        this.capacity = this.capacity * 2;                       4 
        let newNumbers: number[] = new Array(this.capacity);     4个
        对于(让 i = 0;i < this.length;i++){
            newNumbers[i] = this.numbers[i];
        }
        newNumbers[this.length] = 值;
        this.numbers = newNumbers;
        这个。长度++;
    }
}
class NumberList {
    private numbers: number[] = new Array(1);                   1
    private length: number = 0;
    private capacity: number = 1;                               1

    at(index: number): number {
        if (index >= this.length) throw new RangeError();
        return this.numbers[index];                             2
    }

    append(value: number) {
        if (this.length < this.capacity) {                      3
            this.numbers[length] = value;                       3
            this.length++;                                      3
            return;
        }

        this.capacity = this.capacity * 2;                      4
        let newNumbers: number[] = new Array(this.capacity);    4
        for (let i = 0; i < this.length; i++) {
            newNumbers[i] = this.numbers[i];
        }
        newNumbers[this.length] = value;
        this.numbers = newNumbers;
        this.length++;
    }
}

  • 1 尽管列表是空的,但我们从容量 1 开始。
  • 1 Even though the list is empty, we start with a capacity of 1.
  • 2 访问元素与之前的实现相同。
  • 2 Accessing an element is identical to the previous implementation.
  • 3 如果数组没有填满,我们可以简单地添加元素并更新长度。
  • 3 If the array is not filled to capacity, we can simply add the element and update the length.
  • 4 如果我们达到了容量,我们需要分配一个新数组并复制元素,但我们通过将容量加倍来实现这一点,以便将来的追加不需要重新分配。
  • 4 If we’re at capacity, we need to allocate a new array and copy elements, but we do this by doubling the capacity so that future appends do not require a reallocation.

其他的线性数据结构,比如栈和堆,也可以用类似的方式实现。这些数据结构针对读取访问进行了优化,这始终非常高效。使用额外的容量可以使大多数写入变得高效,但有些写入在数据结构已满时需要将所有元素移动到一个新数组,这是低效的。还有内存开销,因为列表分配的元素多于正在使用的元素,以便为将来的追加腾出空间。

Other linear data structures, such as stacks and heaps, can be implemented in a similar way. These data structures are optimized for read access, which is always extremely efficient. Using the extra capacity makes most writes efficient, but some writes, when the data structure is filled to capacity, require moving all elements to a new array, which is inefficient. There is also memory overhead, as the list allocates more elements than there are in use to make room for future appends.

2.5.4.二叉树

2.5.4. Binary trees

让我们看看另一种类型的数据结构:一种我们可以在多个地方追加项目的数据结构。这种数据结构的一个示例是二叉树,其中节点可以附加到没有两个子节点的任何节点。

Let’s look at another type of data structure: a data structure in which we can append items in multiple places. An example of such a data structure is a binary tree, in which nodes can be appended to any node that doesn’t have two children.

一种选择是将二叉树表示为数组。树的第一层,即根,最多有一个节点。树的第二层最多有两个节点:根节点的子节点。第三层最多有四个节点:前一层的孩子两个节点等等。一般来说,对于有N层次的树,一棵二叉树最多可以有1 + 2 + ... + 2 N–1 个节点,也就是2 N –1。

One option is to represent a binary tree as an array. The first level of the tree, the root, has at most one node. The second level of the tree has at most two nodes: the children of the root. The third level has at most four nodes: the children of the previous two nodes and so on. In general, for a tree with N levels, a binary tree can have at most 1 + 2 + ... + 2N–1 nodes, which is 2N–1.

我们可以通过将每个级别放在前一个级别之后来将二叉树存储在数组中。如果树不完整(并非所有级别都有所有节点),我们将缺失的节点标记为undefined。这种表示的一个优点是很容易从父节点获取其子节点:如果父节点位于 index i,则左子节点位于 index 2*i,右子节点位于 index 2*i+1

We can store a binary tree in an array by placing each level after the previous one. If the tree is not complete (not all levels have all the nodes), we mark the missing nodes as undefined. An advantage of this representation is that it’s very easy to get from a parent to its children: if the parent is at index i, the left child node is at index 2*i, and the right child node is at index 2*i+1.

图 2.10显示了我们如何将二叉树表示为固定大小的数组。

Figure 2.10 shows how we can represent a binary tree as a fixed-size array.

图 2.10。二叉树表示为固定大小的数组。缺少的节点(2 的右子节点)是数组中未使用的元素。节点之间的父子关系是隐式的,因为可以根据父节点的索引计算子节点的索引,反之亦然。

只要我们不更改树中的级别数,附加节点也是有效的。但是,一旦我们增加了层级,我们不仅要复制整棵树,还需要将数组的大小加倍,以便为所有可能的新节点腾出空间,如以下清单所示。这类似于高效的列表实现。

Appending a node is also efficient as long as we don’t change the number of levels in the tree. As soon as we increase the level, though, we not only have to copy the whole tree, but also need to double the size of the array to make room for all the new possible nodes, as shown in the following listing. This is similar to the efficient list implementation.

清单 2.16。基于数组的二叉树实现
类树 {
    节点:(数字 | 未定义)[] = [];                 1个

    left_child_index(索引:数字):数字{
        返回索引 * 2;                               2个
    }

    right_child_index(索引:数字):数字{
        返回索引 * 2 + 1;                           2个
    }

    add_level() {
        让 newNodes: (number | undefined)[] =
            新数组(this.nodes.length * 2 + 1);       3个

        for (let i = 0; i < this.nodes.length; i++) {
            newNodes[i] = this.nodes[i];                3个
        }
        this.nodes = newNodes;
    }
}
class Tree {
    nodes: (number | undefined)[] = [];                 1

    left_child_index(index: number): number {
        return index * 2;                               2
    }

    right_child_index(index: number): number {
        return index * 2 + 1;                           2
    }

    add_level() {
        let newNodes: (number | undefined)[] =
            new Array(this.nodes.length * 2 + 1);       3

        for (let i = 0; i < this.nodes.length; i++) {
            newNodes[i] = this.nodes[i];                3
        }
        this.nodes = newNodes;
    }
}

  • 1 节点存储为数值数组或未定义以表示间隙。
  • 1 Nodes are stored as an array of number values or undefined to represent gaps.
  • 2 给定父节点的索引,计算左右子节点的索引。
  • 2 Compute the index of left and right children nodes given the index of the parent.
  • 3 添加新级别的容量会使阵列的大小加倍并重新定位节点。
  • 3 Adding capacity for a new level doubles the size of the array and relocates nodes.

这种实现的缺点是,如果树是稀疏的,所需的额外空间量可能是不可接受的(图 2.11)。

The drawback of this implementation is that the amount of additional space required can be unacceptable if the tree is sparse (figure 2.11).

图 2.11。只有三个节点的稀疏二叉树仍然需要一个包含七个元素的数组才能正确表示。如果节点 9 有一个孩子,数组大小将变为 15。

由于额外的空间开销,二叉树通常使用引用以更紧凑的表示形式表示。一个节点存储一个值和对其子节点的引用。

Because of the extra-space overhead, binary trees are usually represented with a more compact representation using references. A node stores a value and references to its children.

清单 2.17。紧凑的二叉树实现
树节点类 {
    值:数字;                  1
    左:TreeNode | 不明确的;     2
    右:TreeNode | 不明确的;    2个

    构造函数(值:数字){
        this.value = 值;
        this.left = undefined;
        this.right = undefined;
    }
}
class TreeNode {
    value: number;                  1
    left: TreeNode | undefined;     2
    right: TreeNode | undefined;    2

    constructor(value: number) {
        this.value = value;
        this.left = undefined;
        this.right = undefined;
    }
}

  • 1 每个节点存储一个值。
  • 1 Each node stores a value.
  • 2 Left 和 right 引用其他节点,或者如果节点没有子节点则未定义。
  • 2 Left and right refer to other nodes or are undefined if the node doesn’t have a child.

通过此实现,树由对其根节点的引用表示。从那里,在左右孩子之后,我们可以访问树中的任何节点。在任何地方附加一个节点只需要分配一个新节点并设置其父节点的leftor属性。图 2.12显示了我们如何使用引用来表示稀疏树。 right

With this implementation, a tree is represented by a reference to its root node. From there, following left and right children, we can access any node in the tree. Appending a node anywhere involves just allocating a new node and setting the left or right property of its parent. Figure 2.12 shows how we can represent a sparse tree using references.

图 2.12。使用引用表示的稀疏树。右图将节点数据结构表示为值、左引用、右引用。

尽管引用本身需要一些非零内存来表示,但所需的空间量与节点数成正比。对于稀疏树,这比基于数组的实现要好得多,其中空间随着层数呈指数增长。

Although the references themselves require some nonzero memory to represent, the amount of space required is proportional to the number of nodes. For sparse trees, this is much better than the array-based implementation, in which space grows exponentially with the number of levels.

一般来说,元素可以添加到多个地方的稀疏数据结构,我们希望有很多“差距”,通过让元素引用其他元素更好地表示,而不是将整个数据结构放在一个固定大小的数组中这最终会产生不可接受的开销。

In general, sparse data structures where elements can be added in multiple places and we expect to have a lot of “gaps” are better represented by having elements refer to other elements, as opposed to placing the whole data structure in a fixed-size array that would end up having unacceptable overhead.

2.5.5.关联数组

2.5.5. Associative arrays

一些编程语言提供其他类型的数据结构作为原语,并具有内置语法支持。一种常见的此类类型是关联数组,也称为字典哈希表。这种类型的数据结构表示一组键值对,其中,给定一个键,可以有效地检索值。

Some programming languages provide other types of data structures as primitives, with built-in syntax support. A common such type is the associative array, also known as dictionary or hash table. This type of data structure represents a set of key-value pairs where, given a key, the value can be retrieved efficiently.

尽管您在遵循前面的代码示例时可能会想到什么,但 Java-Script/TypeScript 数组是关联数组。这些语言不提供固定大小的数组原始类型。代码示例展示了如何在固定大小的数组上实现数据结构。固定大小的数组假定非常有效的索引和不可变的大小。在 JavaScript/TypeScript 中并非如此。我们查看固定大小数组而不是关联数组的原因是关联数组数据结构可以用数组和引用来实现。出于说明目的,我们将 TypeScript 数组视为固定大小,因此代码示例可以直接翻译成大多数其他流行的编程语言。

Despite what you may have thought as you followed the previous code examples, Java-Script/TypeScript arrays are associative arrays. The languages do not provide a fixed-size array primitive type. The code examples show how data structures can be implemented over fixed-size arrays. A fixed-size array assumes extremely efficient indexing and immutable size. This is not really the case in JavaScript/TypeScript. The reason we looked at fixed-size arrays instead of associative arrays is that an associative array data structure can be implemented with arrays and references. For illustrative purposes, we treated TypeScript arrays as fixed-size, so the code samples can be directly translated into most other popular programming languages.

Java 和 C# 等语言提供字典或哈希映射作为其库的一部分,而数组和引用是基本类型。JavaScript 和 Python 提供关联数组作为原始类型,但它们的运行时也使用数组和引用来实现它们。数组和引用是表示特定内存布局和访问模型的较低级别的构造,而关联数组是较高级别的抽象。

Languages such as Java and C# provide dictionaries or hash maps as part of their library, whereas arrays and references are primitives. JavaScript and Python provide associative arrays as primitive types, but their run times also implement them with arrays and references. Arrays and references are lower-level constructs that represent certain memory layouts and access models, whereas associative arrays are higher-level abstractions.

关联数组通常实现为固定大小的列表数组。哈希函数采用任意类型的键并返回固定大小数组的索引。键值对被添加到数组中给定索引处的列表或从列表中检索。使用该列表是因为多个键可以散列到同一个索引(图 2.13)。

An associative array is often implemented as a fixed-size array of lists. A hash function takes a key of an arbitrary type and returns an index to the fixed-size array. The key-value pair is added to or retrieved from the list at the given index in the array. The list is used because multiple keys can hash to the same index (figure 2.13).

图 2.13。作为列表数组实现的关联数组。此实例包含键值映射 0 → 10、→ 9、5 → 10 和 42 → 0。

通过键查找值涉及找到键值对所在的列表,遍历它直到找到键,然后返回值。如果列表变得太长,查找时间会增加,因此高效的关联数组实现通过增加数组的大小来重新平衡,从而使列表更小。

Looking up a value by key involves finding the list where the key-value pair sits, traversing it until the key is found, and returning the value. If lists become too long, lookup time increases, so efficient associative array implementations rebalance by increasing the size of the array, thus making the lists smaller.

一个好的散列函数可以确保键通常在列表中均匀分布,从而使列表的长度相似。

A good hashing function ensures that keys usually get distributed across the lists evenly so that the lists are similar in length.

2.5.6.实施权衡

2.5.6. Implementation trade-offs

在上一节中,我们了解了数组和引用如何足以实现其他数据结构。根据预期的访问模式(例如读取与写入频率)和数据的预期形状(密集与稀疏),我们可以选择正确的基元来表示数据结构的组件并将它们组合起来以获得最有效的实现。

In the preceding section, we saw how arrays and references are enough to implement other data structures. Depending on the expected access patterns (such as read versus write frequency) and expected shape of the data (dense versus sparse), we can pick the right primitives to represent components of the data structure and combine them to get the most efficient implementation.

固定大小的数组具有极快的读取/更新能力,可以轻松表示密集数据。对于可变大小的数据结构,引用在追加时表现更好,并且可以更轻松地表示稀疏数据。

Fixed-size arrays have extremely fast read/update capabilities and can easily represent dense data. For variable-size data structures, references perform better on append and can represent sparse data more easily.

2.5.7.锻炼

2.5.7. Exercise

1个

哪种数据结构最适合以随机顺序访问其元素?

  1. 链表
  2. 大批
  3. 字典
  4. 队列

1

Which data structure is best suited for accessing its elements in random order?

  1. Linked list
  2. Array
  3. Dictionary
  4. Queue

概括

Summary

  • 永不返回(永远运行或抛出异常)的函数应该声明为返回空类型。空类型可以实现为无法实例化的类或没有元素的枚举。
  • Functions that never return (run forever or throw exceptions) should be declared as returning the empty type. An empty type can be implemented as a class that cannot be instantiated or an enum with no elements.
  • 完成执行但未返回有意义结果的函数应声明为返回单元类型(void在大多数语言中)。单元类型可以实现为单例或具有单个元素的枚举。
  • Functions that finish executing but don’t return a meaningful result should be declared as returning the unit type (void in most languages). A unit type can be implemented as a singleton or an enum with a single element.
  • 布尔表达式求值通常是短路的,因此操作数的顺序会影响它们中的哪些被求值。
  • Boolean expression evaluation is usually short-circuited, so the order of the operands can affect which of them get evaluated.
  • 固定宽度的整数类型可能会溢出。溢出的默认行为是特定于语言的。所需的行为取决于场景。
  • Fixed-width integer types can overflow. The default behavior on overflow is language-specific. The desired behavior depends on the scenario.
  • 浮点数是近似表示的,因此与其比较两个值是否相等,不如检查它们是否在EPSILON彼此之内。
  • Floating-point numbers are represented approximately, so instead of comparing two values for equality, it’s better to check whether they are within EPSILON of each other.
  • 文本由字素组成,字素由一个或多个 Unicode 代码点表示,每个代码点都编码为一个或多个字节。字符串操作库使我们免受编码和表示的复杂性影响,因此最好依赖它们而不是直接操作文本。
  • Text consists of graphemes, which are represented by one or more Unicode code points, each of which is encoded as one or more bytes. String--manipulation libraries shield us from the complexities of encoding and representation, so it’s best to rely on them rather than manipulate text directly.
  • 固定大小的数组和引用是数据结构的构建块。根据数据访问模式和密度,我们可以选择一个或另一个,或两者的组合,以有效地实现任何数据结构,无论多么复杂。
  • Fixed-size arrays and references are the building blocks of data structures. Depending on data access patterns and density, we can choose one or the other, or a combination of the two, to implement any data structure efficiently, no matter how complex.

习题答案

Answers to exercises

设计不返回值的函数

Designing functions that don’t return values

1个

c—函数不返回任何有意义的东西,所以void单位类型是一个很好的返回类型。

1

c—The function doesn’t return anything meaningful, so the void unit type is a good return type.

2个

a——函数永远不会返回,所以空类型never是一个很好的返回类型。

2

a—The function never returns, so the empty type never is a good return type.

 

 

布尔逻辑和短路

Boolean logic and short circuits

1个

b—计数器只增加一次,因为函数返回false,所以布尔表达式被短路。

1

b—The counter is incremented only once because the function returns false, so the Boolean expression is short-circuited.

 

 

数值类型的常见陷阱

Common pitfalls of numerical types

1个

c—false由于浮点数舍入,表达式的计算结果为。

1

c—The expression evaluates to false because of float rounding.

2个

c—因为标识符需要是唯一的,所以出错是首选行为。

2

c—Because identifiers need to be unique, erroring out is the preferred behavior.

 

 

编码文本

Encoding text

1个

d—UTF-8 是可变长度编码。

1

d—UTF-8 is a variable-length encoding.

2个

c—UTF-32为定长编码;所有字符都以四个字节编码。

2

c—UTF-32 is a fixed-length encoding; all characters are encoded in four bytes.

 

 

使用数组和引用构建数据结构

Building data structures with arrays and references

3个

b—数组最适合随机访问。

3

b—Arrays are best suited for random access.

 

 

第3章。作品

Chapter 3. Composition

本章涵盖

This chapter covers

  • 将类型组合成复合类型
  • Combining types into compound types
  • 将类型组合为非此即彼类型
  • Combining types as either-or types
  • 实施访问者模式
  • Implementing visitor patterns
  • 代数数据类型
  • Algebraic data types

第 2 章中,我们研究了一些构成类型系统构建块的常见原始类型。在本章中,我们将研究将它们组合起来定义新类型的方法。

In chapter 2, we looked at some common primitive types that form the building blocks of a type system. In this chapter, we’ll look at ways to combine them to define new types.

我们将介绍复合类型,它将几种类型的值组合在一起。我们将研究命名成员如何赋予数据意义并降低误解的可能性,以及我们如何确保值在需要满足特定约束时格式正确。

We’ll cover compound types, which group values of several types. We’ll look at how naming members gives meaning to data and lowers the chance of misinterpretation, and how we can ensure that values are well-formed when they need to meet certain constraints.

接下来,我们将讨论 either-or 类型,它包含来自多种类型之一的单个值。我们将研究一些常见类型,例如可选类型、任一种类型和变体,以及这些类型的一些应用。例如,我们将看到返回结果错误通常比返回结果错误更安全。

Next, we’ll go over either-or types, which contain a single value from one of several types. We will look at some common types such as optional types, either types, and variants, as well as a few applications of these types. We’ll see, for example, how returning a result or an error is usually safer than returning a result and an error.

作为非此即彼类型的应用,我们将研究访问者设计模式,并将利用类层次结构的实现与使用变体来存储和操作对象的实现进行对比。

As an application of either-or types, we’ll take a look at the visitor design pattern and contrast an implementation that leverages class-hierarchies with an implementation that uses a variant to store and operate on objects.

最后,我们将提供代数数据类型 (ADT) 的描述,并了解它们与本章讨论的主题有何关联。

Finally, we’ll provide a description of algebraic data types (ADTs) and see how they relate to the topics discussed in this chapter.

3.1. 复合类型

3.1. Compound types

组合类型最明显的方法是将它们分组以形成新类型。让我们在平面上取一对 X 和 Y 坐标。X 和 Y 坐标都具有类型number。平面上的一个点同时具有 X 和 Y 坐标,因此它将这两种类型组合成一种新类型,其中值是数字对。

The most obvious way to combine types is to group them to form new types. Let’s take a pair of X and Y coordinates on a plane. Both X and Y coordinates have the type number. A point on the plane has both an X and a Y coordinate, so it combines the two types into a new type in which values are pairs of numbers.

通常,以这种方式组合一种或多种类型会为我们提供一种新类型,其中的值是组件类型的所有可能组合(图 3.1)。

In general, combining one or more types this way gives us a new type in which the values are all the possible combinations of the component types (figure 3.1).

图 3.1。组合两种类型,以便生成的类型包含它们各自的值。每个表情符号代表其中一种类型的值。括号将组合类型的值表示为原始类型的成对值。

请注意,我们谈论的是组合类型的值,而不是它们的操作。当我们在第 8 章中查看面向对象编程的元素时,我们将看到操作是如何组合的。现在,我们将坚持价值观。

Note that we’re talking about combining values of the types, not their operations. We’ll see how operations combine when we look at elements of object-oriented programming in chapter 8. For now, we’ll stick to values.

3.1.1.元组

3.1.1. Tuples

假设我们要计算定义为坐标对的两点之间的距离。我们可以定义一个函数,获取第一个点的 X 坐标和 Y 坐标,以及第二个点的 X 坐标和 Y 坐标,然后计算两者之间的距离,如以下清单所示。

Let’s say we want to compute the distance between two points defined as pairs of coordinates. We can define a function that takes the X coordinate and Y coordinate of the first point, and the X coordinate and the Y coordinate of the second point, and then computes the distance between the two, as shown in the following listing.

清单 3.1。两点之间的距离
函数距离(x1:数字,y1:数字,x2:数字,y2:数字)
    : 数字 {
    返回 Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
}
function distance(x1: number, y1: number, x2: number, y2: number)
    : number {
    return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
}

这行得通,但并不理想:如果我们处理的是点,则x1没有相应的 Y 坐标是没有意义的。我们的应用程序可能需要在多个地方操作点,因此我们可以将它们分组在一个元组中,而不是传递独立的 X 和 Y 坐标。

This works, but it’s not ideal: if we are dealing with points, x1 is meaningless without the corresponding Y coordinate. Our application likely needs to manipulate points in multiple places, so instead of passing around independent X and Y coordinates, we could group them in a tuple.

元组类型

元组类型由一组组件类型组成,我们可以通过它们在元组中的位置来访问它们。元组提供了一种以特殊方式对数据进行分组的方法,允许我们将多个不同类型的值作为单个变量传递。

Tuple types consist of a set of component types, which we can access by their position in the tuple. Tuples provide a way to group data in an ad hoc way, allowing us to pass around several values of different types as a single variable.

使用元组,我们可以将成对的 X 和 Y 坐标一起作为点传递。这使得代码更易于阅读和编写。它更容易阅读,因为现在很清楚我们正在处理点,而且它更容易编写,因为我们可以简单地使用point: Pointinstead of x: number, y: number,如下一个清单所示。

Using tuples, we can pass around pairs of X and Y coordinates together as points. This makes the code both easier to read and easier to write. It’s easier to read as it is now clear that we are dealing with points, and it’s easier to write as we can simply use point: Point instead of x: number, y: number, as shown in the next listing.

清单 3.2。定义为元组的两点之间的距离
输入 Point = [number, number];          1个

函数距离(点 1:,点 2:):数字 {
    返回 Math.sqrt(
        ( point1[0] - point2[0] ) ** 2 + ( point1[1] - point2[1] ) ** 2);
}
type Point = [number, number];          1

function distance(point1: Point, point2: Point): number {
    return Math.sqrt(
        (point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2);
}

  • 1 我们将新类型 Point 定义为数字元组。
  • 1 We define a new type Point to be a tuple of numbers.

当我们需要从一个函数返回多个值时,元组也很有用,如果没有对值进行分组的方法,我们就不能轻易地做到这一点。另一种方法是使用out参数,由函数更新的参数,但这会使代码更难理解。

Tuples are also useful when we need to return multiple values from a function, which we can’t easily do without a way to group values. The alternative is to use out parameters, arguments that are updated by the function, but that makes the code harder to follow.

DIY元组

大多数语言都将元组作为内置语法或作为其库的一部分提供,但让我们看看如果元组不可用,我们将如何实现它。在下面的代码中,我们将实现一个具有两种组件类型的通用元组,也称为pair

Most languages offer tuples as built-in syntax or as part of their library, but let’s look at how we would implement a tuple if it were unavailable. In the following code we’ll implement a generic tuple with two component types, also known as a pair.

清单 3.3。对型
类对 <T1, T2> {
    m0: T1;                1
    立方米:T2;                1个

    构造函数(m0:T1,m1:T2){
        这个.m0 = m0;
        这个.m1 = m1;
    }
}

输入 Point = Pair<number, number> ;

函数距离(点 1:点,点 2:点):数字 {
    返回 Math.sqrt(
        ( point1.m0 - point2.m0 ) ** 2 + ( point1.m1 - point2.m1 ) ** 2);
}
class Pair<T1, T2> {
    m0: T1;                1
    m1: T2;                1

    constructor(m0: T1, m1: T2) {
        this.m0 = m0;
        this.m1 = m1;
    }
}

type Point = Pair<number, number>;

function distance(point1: Point, point2: Point): number {
    return Math.sqrt(
        (point1.m0 - point2.m0) ** 2 + (point1.m1 - point2.m1) ** 2);
}

  • 1 Pair类型包含一个T1类型的值和一个T2类型的值。
  • 1 The Pair type contains a value of type T1 and a value of type T2.

将类型视为可能值的集合,如果 X 坐标可以是定义的集合中的任何值number,并且类似地,Y 坐标可以是定义的集合中的任何值number,则Point元组可以是定义为集合中的任何值对<number, number>

Looking at types as sets of possible values, if the X coordinate can be any value in the set defined by number and, similarly, the Y coordinate can be any value in the set defined by number, the Point tuple can be any value in the set defined as the pair <number, number>.

3.1.2.赋予意义

3.1.2. Assigning meaning

将点定义为数字对是可行的,但我们失去了一些意义:我们可以将一对数字解释为 X 和 Y 坐标或 Y 和 X 坐标(图 3.2)。

Defining points as pairs of numbers works, but we lose some meaning: we can interpret a pair of numbers as either X and Y coordinates or Y and X coordinates (figure 3.2).

图 3.2。对 (1, 5) 的两种解释方式:作为 X 坐标 1 和 Y 坐标 5 的点 A,或作为 X 坐标 5 和 Y 坐标 1 的点 B。

到目前为止,在我们的示例中,我们假设第一个分量是 X 坐标,第二个分量是 Y 坐标。这行得通,但有出错的余地。如果我们可以在类型系统中对含义进行编码,并确保没有空间将 X 误解为 Y 或将 Y 误解为 X,那就更好了。我们可以通过使用记录类型来做到这一点

In our examples so far, we assumed that the first component is the X coordinate and the second the Y coordinate. This works but leaves room for error. It is better if we can encode the meaning within the type system and ensure that there is no room to misinterpret X as Y or Y as X. We can do this by using a record type.

记录类型

记录类型,类似于元组,组合了多种其他类型。记录类型允许我们给它们的组件名称并通过名称访问它们,而不是通过它们在元组中的位置访问组件值。记录类型被称为recordstruct使用不同的语言。

Record types, similar to tuples, combine multiple other types. Instead of the component values being accessed by their position in the tuple, record types allow us to give their components names and access them by name. Record types are known as record or struct in different languages.

如果我们将 our 定义Point为记录,我们可以将名称x和分配y给两个组件,并且不会留下歧义的余地,如下一个清单所示。

If we define our Point as a record, we can assign the names x and y to the two components and leave no room for ambiguity, as the next listing shows.

清单 3.4。定义为记录的两点之间的距离
类点{
    x:数字;        1 
    y:数字;        1个

    构造函数(x:数字,y:数字){
        这个.x = x;
        这个.y = y;
    }
}

函数距离(点 1:点,点 2:点):数字 {
    返回 Math.sqrt(
        ( point1.x - point2.x ) ** 2 + ( point1.y - point2.y ) ** 2);
}
class Point {
    x: number;        1
    y: number;        1

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

function distance(point1: Point, point2: Point): number {
    return Math.sqrt(
        (point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2);
}

  • 1 Point 定义了x 和y 成员,所以很清楚哪个坐标是由哪个组件编码的。
  • 1 Point defines x and y members, so it is clear which coordinate is encoded by which component.

根据经验,通常最好使用命名组件定义记录,而不是四处传递元组。元组没有命名它们的组件这一事实为误解留下了空间。元组在效率或功能方面并没有真正提供比记录更好的东西,除了我们通常可以在我们使用它们的地方内联声明它们,而我们通常必须为记录提供单独的定义。在大多数情况下,单独的定义值得添加,因为它为我们的变量提供了额外的含义。

As a rule of thumb, it’s usually best to define records with named components instead of passing tuples around. The fact that tuples do not name their components leaves room for misinterpretation. Tuples don’t really provide anything better than records in terms of efficiency or functionality, except that we can usually declare them inline where we are using them, whereas we usually have to provide a separate definition for records. In most cases, the separate definition is worth adding, as it provides extra meaning to our variables.

3.1.3.保持不变量

3.1.3. Maintaining invariants

在记录类型可以有关联方法的语言中,通常有一种方法来定义其成员的可见性。成员可以定义为public(可从任何地方访问)、private(仅可从记录内访问)等。在 TypeScript 中,成员默认是公共的。

In languages in which record types can have associated methods, there is usually a way to define the visibility of their members. A member can be defined as public (accessible from anywhere), private (accessible only from within the record), and so on. In TypeScript, members are public by default.

一般来说,当我们定义记录类型时,如果成员是独立的并且可以变化而不会引起问题,那么将它们标记为公共就可以了。这是定义为成对的 X 和 Y 坐标的点的情况:当点在平面上移动时,其中一个坐标可以独立于另一个坐标而变化。

In general, when we define record types, if the members are independent and can vary without causing issues, it’s fine to mark them as public. This is the case with points defined as pairs of X and Y coordinates: one of the coordinates can change independently of the other coordinate as a point moves on the plane.

让我们举另一个例子,其中成员不能独立变化而不会引起问题:我们在第 2 章中看到的货币类型,由dollar数量和cents数量组成。让我们使用以下定义格式正确的货币金额的规则来增强类型的定义:

Let’s take another example in which the members can’t vary independently without causing issues: the currency type we looked at in chapter 2, formed by a dollar amount and a cents amount. Let’s enhance the definition of the type with the following rules that define a well-formed currency amount:

  • 美元金额必须是等于或大于 0 的整数,并且可以安全地表示为类型number
  • The dollar amount must be an integer equal to or greater than 0 and safely representable as a number type.
  • 分金额必须是等于或大于 0 的整数,并且可以安全地表示为类型number
  • The cent amount must be an integer equal to or greater than 0 and safely representable as a number type.
  • 我们不应该超过 99 美分;每 100 美分应转换为一美元。
  • We shouldn’t have more than 99 cents; every 100 cents should be converted to a dollar.

此类确保值格式正确的规则也称为不变量,因为即使构成复合类型的值发生变化,它们也不应发生变化。如果我们公开成员,外部代码可以更改它们,我们最终可能会得到格式错误的记录,如下一个清单所示。

Such rules that ensure a value is well-formed are also called invariants, as they shouldn’t change even as the values that make up the composite type change. If we make the members public, external code can change them, and we can end up with ill-formed records, as shown in the next listing.

清单 3.5。货币格式错误
类货币{
    美元:数字;
    分:数量;

    构造函数(美元:数字,美分:数字){
        如果 (!Number.isSafeInteger(cents) || cents < 0)         1
            抛出新的错误();

        美元 = 美元 + Math.floor(cents / 100);          2
        美分 = 美分 % 100;                                  2个

        如果 (!Number.isSafeInteger(dollars) || dollars < 0)     1
            抛出新的错误();

        this.dollars = 美元;
        this.cents = 美分;
    }
}

让金额:货币=新货币(5、50);
金额.cents = 300;                                           3个
class Currency {
    dollars: number;
    cents: number;

    constructor(dollars: number, cents: number) {
        if (!Number.isSafeInteger(cents) || cents < 0)        1
            throw new Error();

        dollars = dollars + Math.floor(cents / 100);          2
        cents = cents % 100;                                  2

        if (!Number.isSafeInteger(dollars) || dollars < 0)    1
            throw new Error();

        this.dollars = dollars;
        this.cents = cents;
    }
}

let amount: Currency = new Currency(5, 50);
amount.cents = 300;                                           3

  • 1 构造函数确保我们具有有效的美元和美分值。
  • 1 Constructor ensures that we have valid dollars and cents values.
  • 2 每 100 美分转换为 1 美元。
  • 2 Every 100 cents gets converted to a dollar.
  • 3 不幸的是,让成员公开仍然允许外部代码生成无效对象。
  • 3 Unfortunately, having the members public still allows external code to make an invalid object.

通过将成员设为私有并提供更新它们的方法以确保保持不变量,可以防止这种情况,如以下清单所示。如果我们处理所有不变量无效的情况,我们可以确保对象始终处于有效状态,因为更改它会为我们提供另一个格式正确的对象或导致异常。

This situation can be prevented by making the members private and providing methods to update them that ensure the invariants are maintained, as shown in the following listing. If we handle all cases in which invariants would be invalidated, we can ensure that an object is always in a valid state, as changing it would give us another well-formed object or result in an exception.

清单 3.6。货币保持不变量
类货币{
    私人美元:数量= 0;                                     1个
    私分:数量=0;                                       1个

    构造函数(美元:数字,美分:数字){
        this.assignDollars(美元);
        this.assignCents(美分);
    }

    getDollars(): 数字 {
        返回 this.dollars;
    }

    assignDollars(美元:数字){
        如果 (!Number.isSafeInteger(dollars) || dollars < 0)            2
            抛出新的错误();

        this.dollars = 美元;
    }

    getCents(): 数字 {
        返回this.cents;
    }

    assignCents(美分:数字){
        如果 (!Number.isSafeInteger(cents) || cents < 0)                2
            抛出新的错误();

        this.assignDollars(this.dollars + Math.floor(cents / 100));  3个
        this.cents = 美分 % 100;
    }
}
class Currency {
    private dollars: number = 0;                                     1
    private cents: number = 0;                                       1

    constructor(dollars: number, cents: number) {
        this.assignDollars(dollars);
        this.assignCents(cents);
    }

    getDollars(): number {
        return this.dollars;
    }

    assignDollars(dollars: number) {
        if (!Number.isSafeInteger(dollars) || dollars < 0)           2
            throw new Error();

        this.dollars = dollars;
    }

    getCents(): number {
        return this.cents;
    }

    assignCents(cents: number) {
        if (!Number.isSafeInteger(cents) || cents < 0)               2
            throw new Error();

        this.assignDollars(this.dollars + Math.floor(cents / 100));  3
        this.cents = cents % 100;
    }
}

  • 1 将美元和美分设为私有可确保外部代码无法绕过验证。
  • 1 Making dollars and cents private ensures that external code can’t bypass validation.
  • 2 如果美元或美分金额无效(负数或非安全整数),则抛出异常。
  • 2 If the dollar or cent amount is invalid (negative or nonsafe integer), throw an exception.
  • 3 通过将 100 美分转换为美元来标准化该值。
  • 3 Normalize the value by converting 100 cents to dollars.

外部代码现在必须通过assignDollars()assignCents()函数,以确保维护所有不变量:如果提供的值无效,则会抛出异常。如果美分数大于 100,则转换为美元。

External code now has to go through the assignDollars() and assignCents() functions, which ensure that all invariants are maintained: if the provided values are invalid, exceptions are thrown. If the number of cents is larger than 100, it is converted to dollars.

一般来说,如果没有要强制执行的不变量,例如平面上一个点的独立 X 和 Y 分量,我们应该可以直接访问记录的公共成员。另一方面,如果我们有一组规则来定义记录的格式正确意味着什么,我们应该使用私有成员和方法来更新它们以确保规则得到执行。

In general, we should be fine providing direct access to public members of a record if there are no invariants to be enforced, such as the independent X and Y components of a point on a plane. On the other hand, if we have a set of rules that define what it means for a record to be well-formed, we should use private members and methods to update them to ensure that the rules are enforced.

另一种选择是使成员不可变,如以下清单所示,在这种情况下,我们可以在初始化期间确保记录格式正确,但随后我们可以允许直接访问成员,因为它们不能被更改外部代码。

Another option is to make the members immutable, as shown in the following listing, in which case we can ensure during initialization that the record is well-formed, but then we can allow direct access to the members because they can’t be changed by external code.

清单 3.7。不变的货币
类货币{
    只读美元:数字;                                1
    只读分:数字;                                  1个

    构造函数(美元:数字,美分:数字){
        如果 (!Number.isSafeInteger(cents) || cents < 0)        2
            抛出新的错误();

        美元 = 美元 + Math.floor(cents / 100);         2个
        美分 = 美分 % 100;

        如果 (!Number.isSafeInteger(dollars) || dollars < 0)    2
            抛出新的错误();

        this.dollars = 美元;
        this.cents = 美分;
    }
}
class Currency {
    readonly dollars: number;                                1
    readonly cents: number;                                  1

    constructor(dollars: number, cents: number) {
        if (!Number.isSafeInteger(cents) || cents < 0)       2
            throw new Error();

        dollars = dollars + Math.floor(cents / 100);         2
        cents = cents % 100;

        if (!Number.isSafeInteger(dollars) || dollars < 0)   2
            throw new Error();

        this.dollars = dollars;
        this.cents = cents;
    }
}

  • 1 美元和美分是公开的,但只读,初始化后不能更改。
  • 1 Dollars and cents are public but read-only and can’t be changed after initialization.
  • 2 现在所有验证都在构造函数中进行。
  • 2 All validation takes place in the constructor now.

如果成员是不可变的,我们就不再需要它们的函数来维护不变量。成员设置的唯一时间是在构造期间,因此我们可以将所有验证逻辑移到那里。不可变数据还有其他优点:从不同线程并发访问此数据是安全的,因为数据无法更改。当一个线程修改一个值而另一个线程正在使用它时,可变性会导致数据竞争。

If the members are immutable, we no longer need functions for them to uphold the invariants. The only time when the members are set is during construction, so we can move all the validation logic there. Immutable data has other advantages: accessing this data concurrently from different threads is guaranteed to be safe, as the data can’t change. Mutability can cause data races, when one thread modifies a value while another thread is using it.

具有不可变成员的记录的缺点是我们需要在需要新值时创建一个新实例。根据创建新实例的成本,我们可能会选择可以使用 getter 和 setter 方法就地更新成员的记录,或者我们可能会选择每次更新都需要创建新对象的实现。

The drawback of records with immutable members is that we need to create a new instance whenever we want a new value. Depending on how expensive it is to create new instances, we might opt for a record in which the members can be updated in place by using getter and setter methods, or we might go with an implementation in which each update requires creating a new object.

目标是防止外部代码进行绕过验证规则的更改,方法是将成员设为私有并通过方法路由所有访问,或者将成员设为不可变并在构造函数中应用验证。

The goal is to prevent external code from making changes that bypass our validation rules, either by making members private and routing all access through methods or by making the members immutable and applying validation in the constructor.

3.1.4.锻炼

3.1.4. Exercise

1个

在 3D 空间中定义点的首选方法是什么?

  1. type Point = [number, number, number];
  2. type Point = number | number | number;
  3. type Point = { x: number, y: number, z: number };
  4. type Point = any;

1

What is the preferred way of defining a point in 3D space?

  1. type Point = [number, number, number];
  2. type Point = number | number | number;
  3. type Point = { x: number, y: number, z: number };
  4. type Point = any;

3.2. 用类型表达 either-or

3.2. Expressing either-or with types

到目前为止,我们已经了解了通过对类型进行分组来组合类型,这样值就由来自每个成员类型的一个值组成。我们可以组合类型的另一种基本方法是非此即彼,其中值是一个或多个基础类型的一组可能值中的任何一个(图 3.3

So far, we’ve looked at combining types by grouping them such that values are composed of one value from each of the member types. Another fundamental way in which we can combine types is either-or, in which a value is any one of a possible set of values of one or more underlying types (figure 3.3).

图 3.3。组合两种类型,以便生成的类型允许来自两种类型之一的值。

3.2.1.枚举

3.2.1. Enumerations

让我们从一个非常简单的任务开始:在类型系统中编码星期几。我们可以说星期几是 0 到 6 之间的数字,0 是一周的第一天,6 是最后一天。这不太理想,因为编写代码的多个工程师可能对一周的第一天有不同的看法。美国、加拿大和日本等国家将星期日视为一周的第一天,而 ISO 8601 标准和大多数欧洲国家将星期一视为一周的第一天。

Let’s start with a very simple task: encoding a day of the week in the type systems. We could say the day of the week is a number between 0 and 6, 0 being the first day of the week and 6 being the last one. This is less than ideal, because multiple engineers working on the code might have different opinions of what the first day of the week is. Countries such as the United States, Canada, and Japan consider Sunday to be the first day of the week, whereas the ISO 8601 standard and most European countries consider Monday to be the first day of the week.

清单 3.8。将星期几编码为数字
函数 isWeekend(dayOfWeek: number): 布尔值 {
    返回 dayOfWeek == 5 || 星期几 == 6;         1个
}

函数 isWeekday(dayOfWeek: number): 布尔值 {
    返回 dayOfWeek >= 1 && dayOfWeek <= 5;         2 
}
function isWeekend(dayOfWeek: number): boolean {
    return dayOfWeek == 5 || dayOfWeek == 6;         1
}

function isWeekday(dayOfWeek: number): boolean {
    return dayOfWeek >= 1 && dayOfWeek <= 5;         2
}

  • 1 欧洲开发人员会将第 5 天和第 6 天视为周末(周六和周日)。
  • 1 A European developer would consider days 5 and 6 to be the weekend (Saturday and Sunday).
  • 2 美国开发人员会将第 1 天到第 5 天视为工作日(周一到周五)。
  • 2 An American developer would consider days 1 to 5 to be weekdays (Monday through Friday).

从这个代码示例中可以明显看出,这两个函数不可能都正确。如果 0 代表星期天,isWeekend()则不正确;如果 0 代表星期一,isWeekday()则不正确。不幸的是,因为 0 的含义不是强制的,而是约定俗成的,所以没有自动的方法来防止这个错误。

It should be obvious from this code example that the two functions can’t both be correct. If 0 represents Sunday, isWeekend() is incorrect; if 0 represents Monday, isWeekday() is incorrect. Unfortunately, because the meaning of 0 is not enforced but determined by convention, there is no automatic way to prevent this error.

另一种方法是声明一组常量值来表示星期几,并确保在需要一周中的某一天时使用这些常量。

An alternative is to declare a set of constant values to represent the days of the week and make sure that these constants are used whenever a day of the week is expected.

清单 3.9。用常量编码星期几
常量星期日:数字= 0;
const 星期一:数字 = 1;
const 星期二:数字 = 2;
const 星期三:数字 = 3;
const 星期四:数字 = 4;
const 星期五:数字 = 5;
const 星期六:数字 = 6;

函数 isWeekend(dayOfWeek: number): 布尔值 {
    返回 dayOfWeek ==星期六|| dayOfWeek ==周日;    1个
}

函数 isWeekday(dayOfWeek: number): 布尔值 {
    返回 dayOfWeek >=星期一&& dayOfWeek <=星期五;      1 
}
const Sunday: number = 0;
const Monday: number = 1;
const Tuesday: number = 2;
const Wednesday: number = 3;
const Thursday: number = 4;
const Friday: number = 5;
const Saturday: number = 6;

function isWeekend(dayOfWeek: number): boolean {
    return dayOfWeek == Saturday || dayOfWeek == Sunday;    1
}

function isWeekday(dayOfWeek: number): boolean {
    return dayOfWeek >= Monday && dayOfWeek <= Friday;      1
}

  • 1 我们现在不使用数字,而是使用命名常量来确保一致性。
  • 1 Instead of numbers, we now use named constants to ensure consistency.

这个实现比之前的实现稍微好一点,但仍然存在一个问题:看函数的声明,不清楚 type 的参数的期望值是什么number。刚接触代码的人怎么知道每当他们看到 a 时dayOfWeek: number,他们应该使用其中一个常量?他们可能不知道这些常量存在于某个模块中的某处,相反,他们可以自己解释这些数字,如我们在清单 3.8中的第一个示例。也有人可以使用完全无效的值调用该函数,例如-110。一个更好的解决方案是声明一个星期几的枚举。

This implementation is slightly better than the previous implementation, but there’s still a problem: looking at the declaration of a function, it’s not clear what the expected values are for an argument of type number. How is someone who’s new to the code supposed to know that whenever they see a dayOfWeek: number, they should use one of the constants? They may not be aware that these constants exist somewhere in some module, and instead, they could interpret the number themselves, as in our first example in listing 3.8. Someone can also call the function with completely invalid values, such as -1 or 10. An even better solution is to declare an enumeration for the days of the week.

清单 3.10。将星期几编码为枚举
枚举 DayOfWeek {                                        1
    周日、
    周一、
    周二、
    周三、
    周四、
    周五、
    周六
}

函数 isWeekend(dayOfWeek: DayOfWeek ): boolean {     2
    返回 dayOfWeek == DayOfWeek.Saturday
        || dayOfWeek == DayOfWeek.Sunday;
}

函数 isWeekday(dayOfWeek: DayOfWeek ): boolean {     2
    返回 dayOfWeek >= DayOfWeek.Monday
        && dayOfWeek <= DayOfWeek.Friday;
}
enum DayOfWeek {                                       1
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

function isWeekend(dayOfWeek: DayOfWeek): boolean {    2
    return dayOfWeek == DayOfWeek.Saturday
        || dayOfWeek == DayOfWeek.Sunday;
}

function isWeekday(dayOfWeek: DayOfWeek): boolean {    2
    return dayOfWeek >= DayOfWeek.Monday
        && dayOfWeek <= DayOfWeek.Friday;
}

  • 1 枚举取代常量。
  • 1 An enum replaces the constants.
  • 2 我们现在有一个独特的类型来表示一周中的一天。
  • 2 We now have a distinct type that represents a day of the week.

通过这种方法,我们直接将星期几编码为一个枚举,这有两大优势:星期一和星期日没有歧义,因为它们在代码中拼写出来了。此外,很明显,在查看需要参数的函数声明时dayOfWeek: DayOfWeek,我们应该传入 的成员DayOfWeek,例如DayOfWeek.Tuesday,而不是数字。

With this approach, we directly encode the days of the week in an enumeration that has two big advantages: there is no ambiguity about what is Monday and what is Sunday, as they are spelled out in the code. Also, it’s very clear, when looking at a function declaration expecting a dayOfWeek: DayOfWeek argument, that we should pass in a member of DayOfWeek, such as DayOfWeek.Tuesday, not a number.

这是将一组值组合成新类型的基本示例。该类型的变量可以是提供的值之一。每当我们有一小组可能的值并希望以明确的方式表示它们时,我们就会使用枚举。接下来,让我们看看如何将这个概念应用于类型而不是值。

This is a basic example of combining a set of values into a new type. A variable of that type can be one of the provided values. We would use enumerations whenever we have a small set of possible values and want to represent them in an unambiguous manner. Next, let’s see how we apply this concept to types instead of values.

3.2.2.可选类型

3.2.2. Optional types

假设我们要将string作为用户输入提供的 转换为DayOfWeek. 如果我们可以将字符串解释为星期几,我们想返回一个DayOfWeek值,但如果我们不能解释它,我们想明确地说星期几是undefined。我们可以使用|类型运算符在 TypeScript 中实现这一点,它允许我们组合类型,如以下代码所示。

Let’s say we want to convert a string, provided as user input, to a DayOfWeek. If we can interpret the string as a day of week, we want to return a DayOfWeek value, but if we can’t interpret it, we want to explicitly say that the day of the week is undefined. We can implement this in TypeScript by using the | type operator, which allows us to combine types, as shown in the following code.

清单 3.11。将输入解析为 aDayOfWeekundefined
函数 parseDayOfWeek(input: string): DayOfWeek | 未定义{     1
    开关(输入。toLowerCase()){
        case "星期天": 返回 DayOfWeek.Sunday;

        case "monday": 返回 DayOfWeek.Monday;
        case "tuesday": 返回 DayOfWeek.Tuesday;
        case "wednesday": 返回 DayOfWeek.Wednesday;
        case "thursday": 返回 DayOfWeek.Thursday;
        case "friday": 返回 DayOfWeek.Friday;
        案例“星期六”:返回DayOfWeek.Saturday;
        默认值:返回未定义;                                 2个
    }
}

函数使用输入(输入:字符串){
    让结果:DayOfWeek | undefined = parseDayOfWeek(输入);

    如果(结果 === 未定义){                                     3
        console.log(`无法解析“${input}”`);
    } 别的 {
        让 dayOfWeek: DayOfWeek = 结果;                         4个
        /* 使用星期几 */
    }
}
function parseDayOfWeek(input: string): DayOfWeek | undefined {    1
    switch (input.toLowerCase()) {
        case "sunday": return DayOfWeek.Sunday;

        case "monday": return DayOfWeek.Monday;
        case "tuesday": return DayOfWeek.Tuesday;
        case "wednesday": return DayOfWeek.Wednesday;
        case "thursday": return DayOfWeek.Thursday;
        case "friday": return DayOfWeek.Friday;
        case "saturday": return DayOfWeek.Saturday;
        default: return undefined;                                 2
    }
}

function useInput(input: string) {
    let result: DayOfWeek | undefined = parseDayOfWeek(input);

    if (result === undefined) {                                    3
        console.log(`Failed to parse "${input}"`);
    } else {
        let dayOfWeek: DayOfWeek = result;                         4
        /* Use dayOfWeek */
    }
}

  • 1 该函数返回 DayOfWeek 或未定义。
  • 1 The function returns a DayOfWeek or undefined.
  • 2 如果两种情况都不匹配,我们返回 undefined 以表示我们无法解析输入。
  • 2 If neither case matches, we return undefined to signal that we couldn’t parse the input.
  • 3 检查我们是否解析失败,在这种情况下我们记录错误。
  • 3 Check whether we failed to parse, in which case we log an error.
  • 4 如果结果不是未定义的,我们可以从中提取 DayOfWeek 值并继续使用它。
  • 4 If result is not undefined, we can extract a DayOfWeek value from it and use it going forward.

parseDayOfWeek()函数返回一个DayOfWeekundefined。该use-Input()函数调用此函数,然后尝试解包结果、记录错误或以DayOfWeek它可以使用的值结束。

This parseDayOfWeek() function returns a DayOfWeek or undefined. The use-Input() function calls this function and then tries to unwrap the result, logging an error or ending up with a DayOfWeek value that it can use.

可选类型

可选类型,也称为可能类型,表示另一种类型的可选值T。可选类型的实例可以包含 type 的值(任何值)T或指示不存在 type 值的特殊值T

An optional type, also known as a maybe type, represents an optional value of another type T. An instance of the optional type can hold a value (any value) of type T or a special value indicating the absence of a value of type T.

DIY可选

一些主流编程语言不支持以这种方式组合类型的语法级支持,但一组通用结构可作为库使用。我们的DayOfWeekorundefined示例是可选类型。可选值要么包含其基础类型的值,要么不包含任何值。

Some mainstream programming languages do not have syntax-level support for combining types this way, but a set of common constructs is available as libraries. Our DayOfWeek or undefined example is an optional type. An optional contains either a value of its underlying type or no value.

可选类型通常包装另一种类型作为泛型类型参数提供,并提供几个方法:一个hasValue()方法,它告诉我们是否有一个实际值,以及一个getValue(),它返回该值。在未设置任何值时尝试调用getValue()会导致抛出异常,如下一个清单所示。

An optional type usually wraps another type provided as a generic type argument and provides a couple of methods: a hasValue() method, which tells us whether we have an actual value, and a getValue(), which returns that value. Attempting to call getValue() when no value is set causes an exception to be thrown, as shown in the next listing.

清单 3.12。可选类型
可选类 <T> {                           1
    私有值:T | 不明确的;
    私人分配:布尔值;

    构造函数(值?:T){                  2
        如果(值){
            this.value = 值;
            this.assigned = true;
        } 别的 {
            this.value = undefined;
            this.assigned = false;
        }
    }

    有值():布尔值{
        返回this.assigned;
    }

    getValue(): T {
        如果(!this.assigned)抛出错误();   3个

        返回<T>这个值;
    }
}
class Optional<T> {                          1
    private value: T | undefined;
    private assigned: boolean;

    constructor(value?: T) {                 2
        if (value) {
            this.value = value;
            this.assigned = true;
        } else {
            this.value = undefined;
            this.assigned = false;
        }
    }

    hasValue(): boolean {
        return this.assigned;
    }

    getValue(): T {
        if (!this.assigned) throw Error();   3

        return <T>this.value;
    }
}

  • 1 Optional 包装了一个通用类型 T。
  • 1 Optional wraps a generic type T.
  • 2 value 是可选参数,因为 TypeScript 不支持构造函数重载。
  • 2 value is an optional argument, because TypeScript doesn’t support constructor overloads.
  • 3 如果未分配此 Optional,则尝试获取值会引发异常。
  • 3 If this Optional is not assigned, attempting to get a value throws an exception.

在其他没有 | 的语言中 允许我们定义T | undefined类型的类型运算符,我们将改用可空类型。可为null 的 类型允许or 类型的任何值null,这表示没有值。

In other languages that don’t have a | type operator that allows us to define a T | undefined type, we would use a nullable type instead. A nullable type allows for any value of the type or null, which represents the absence of a value.

你可能想知道为什么这个可选类型有用,考虑到在大多数语言中,引用类型是允许的null,所以已经有一种方法可以在不需要这种类型的情况下编码“无可用值”。

You might wonder why this optional type is useful, considering that in most languages, reference types are allowed to be null, so there is already a way to encode “no value available” without needing such a type.

不同之处在于 usingnull容易出错(请参阅边栏“十亿美元的错误”),因为很难判断变量何时可以或不可以null。我们必须null在整个代码中添加检查,否则有取消引用null变量的风险,这会导致运行时错误。可选类型背后的想法是将null与允许值的范围分离。每当我们看到一个可选的,我们就知道它没有价值。在我们检查我们确实有一个值之后,我们将它从可选中解包并得到一个底层类型的变量。从这里开始,我们知道变量不能是null。这种区别体现在类型系统中,因为“might be null”变量具有不同的类型(DayOfWeek | undefinedOptional<DayOfWeek>) 来自展开的值,我们知道它不能是null( DayOfWeek)。可选类型与其底层类型不兼容会有所帮助,因此我们不会在未显式解包值的情况下意外使用可选类型(可能没有值)而不是其底层类型。

The difference is that using null is error-prone (see the sidebar “A billion-dollar mistake”), as it’s hard to tell when a variable can or can’t be null. We must add null checks all over the code or risk dereferencing a null variable, which results in a run-time error. The idea behind an optional type is to decouple the null from the range of allowed values. Whenever we see an optional, we know that it can have no value. After we check that we indeed have a value, we unwrap it from the optional and get a variable of the underlying type. From here on, we know that the variable cannot be null. This distinction is captured in the type system, as the “might be null” variable has a different type (DayOfWeek | undefined or Optional<DayOfWeek>) from the unwrapped value, which we know can’t be null (DayOfWeek). It helps that an optional type and its underlying type are incompatible, so we can’t accidentally use an optional (which may not have a value) instead of its underlying type without explicitly unwrapping the value.

十亿美元的错误

著名计算机科学家和图灵奖获得者托尼·霍尔爵士称其null为“数十亿美元的错误”。引用他的话说:

Famous computer scientist and Turing Award winner Sir Tony Hoare calls null references his “billion-dollar mistake.” He is quoted as saying:

“我称之为我的十亿美元错误。它是 1965 年空引用的发明。当时,我正在设计面向对象语言中的第一个引用综合类型系统。我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但我无法抗拒放入空引用的诱惑,只是因为它很容易实现。这导致了无数的错误、漏洞和系统崩溃,在过去四十年中可能造成了十亿美元的痛苦和损失。”

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”

在几十年的null取消引用错误之后,越来越清楚的是null,如果 或值的缺失本身不是类型的有效值,则更好。

After decades of null dereference errors, it’s becoming clear that it is better if null, or the absence of the value, is not itself a valid value of a type.

3.2.3.结果或错误

3.2.3. Result or error

让我们扩展我们的DayOfWeek字符串转换示例,以便在我们无法确定值时不返回任何值DayOfWeek,而是返回更详细的错误信息。我们想要区分字符串何时为空以及何时无法解析它。如果我们在文本输入控件后面运行此代码,这将很有用,因为我们希望根据错误(Please enter a day of weekInvalid day of week)向用户显示不同的错误消息。

Let’s extend our DayOfWeek string conversion example so that instead of simply returning no value when we cannot determine the DayOfWeek value, we return more detailed error information. We want to distinguish between when the string is empty and when we are unable to parse it. This is useful if we run this code behind a text input control, as we want to show different error messages to the user, depending on the error (Please enter a day of week versus Invalid day of week).

一个常见的反模式会同时返回一个错误DayOfWeek代码和一个错误代码,如下一个清单所示。如果错误代码指示成功,我们将使用该DayOfWeek值。如果错误代码表示错误,该DayOfWeek值无效,我们不应该使用它。

A common antipattern returns both a DayOfWeek and an error code, as shown in the next listing. If the error code indicates success, we use the DayOfWeek value. If the error code indicates an error, the DayOfWeek value is invalid, and we shouldn’t use it.

清单 3.13。从函数返回结果和错误
枚举输入错误 {                                                      1
    好的,
    没有输入,
    无效的
}

类结果{#B
    错误:输入错误;
    值:星期几;

    构造函数(错误:InputError,值:DayOfWeek){
        this.error = 错误;
        this.value = 值;
    }
}

函数 parseDayOfWeek(输入:字符串):结果{                       2
    如果(输入==“”)
        返回新结果(InputError.NoInput,DayOfWeek.Sunday);      3个

    开关(输入。toLowerCase()){
        案例“星期天”:
            返回新结果(InputError.OK,DayOfWeek.Sunday);       4个
        案例“星期一”:
            返回新结果(InputError.OK,DayOfWeek.Monday);
        案例“星期二”:
            返回新结果(InputError.OK,DayOfWeek.Tuesday);
        案例“星期三”:
            返回新结果(InputError.OK,DayOfWeek.Wednesday);
        案例“星期四”:
            返回新结果(InputError.OK,DayOfWeek.Thursday);
        案例“星期五”:
            返回新结果(InputError.OK,DayOfWeek.Friday);
        案例“星期六”:
            返回新结果(InputError.OK,DayOfWeek.Saturday);
        默认:
            返回新结果(InputError.Invalid,DayOfWeek.Sunday);  5个
    }
}
enum InputError {                                                     1
    OK,
    NoInput,
    Invalid
}

class Result {    #B
    error: InputError;
    value: DayOfWeek;

    constructor(error: InputError, value: DayOfWeek) {
        this.error = error;
        this.value = value;
    }
}

function parseDayOfWeek(input: string): Result {                      2
    if (input == "")
        return new Result(InputError.NoInput, DayOfWeek.Sunday);      3

    switch (input.toLowerCase()) {
        case "sunday":
            return new Result(InputError.OK, DayOfWeek.Sunday);       4
        case "monday":
            return new Result(InputError.OK, DayOfWeek.Monday);
        case "tuesday":
            return new Result(InputError.OK, DayOfWeek.Tuesday);
        case "wednesday":
            return new Result(InputError.OK, DayOfWeek.Wednesday);
        case "thursday":
            return new Result(InputError.OK, DayOfWeek.Thursday);
        case "friday":
            return new Result(InputError.OK, DayOfWeek.Friday);
        case "saturday":
            return new Result(InputError.OK, DayOfWeek.Saturday);
        default:
            return new Result(InputError.Invalid, DayOfWeek.Sunday);  5
    }
}

  • 1 InputError 表示错误代码。
  • 1 InputError represents the error code.
  • 2 结果结合了错误代码和 DayOfWeek 值。
  • 2 Result combines the error code and the DayOfWeek value.
  • 3 如果字符串为空且默认 DayOfWeek,我们返回 NoInput。
  • 3 We return NoInput if the string is empty and a default DayOfWeek.
  • 4 如果我们能够成功解析输入,我们将返回 OK 和解析后的 DayOfWeek。
  • 4 We return OK and the parsed DayOfWeek if we can successfully parse the input.
  • 5 否则,如果解析失败,我们将返回 Invalid 和默认的 DayOfWeek。
  • 5 Otherwise, we return Invalid and a default DayOfWeek if we fail to parse.

这并不理想,因为如果我们不小心忘记检查错误代码,没有什么能阻止我们使用该DayOfWeek成员。现在该值可以是默认值,并且我们不一定能说它是无效的。我们可能会通过系统传播错误,例如将其写入数据库,而没有意识到我们根本不应该使用该值。

This is not ideal because if we accidentally forget to check the error code, nothing prevents us from using the DayOfWeek member. Now the value can be a default, and we aren’t necessarily able to tell that it is invalid. We might propagate the error through the system, such as writing it to a database, without realizing that we shouldn’t have used the value at all.

从类型作为集合的角度来看,我们的结果包含所有可能的错误代码和所有可能的结果的组合(图 3.4)。

Looking at this from the lens of types as sets, our result contains the combination of all possible error codes and all possible results (figure 3.4).

图 3.4。类型的所有可能值作为和Result的组合。那是 21 个值 (3 x 7 )。 InputErrorDayOfWeekInputErrorDayOfWeek

相反,我们应该尝试返回错误有效值。如果我们设法做到这一点,可能值的集合将大大减少,并且我们消除了使用组件是或的组件DayOfWeek的可能性(图 3.5)。 ResultInputErrorNoInputInvalid

Instead, we should try to return either an error or a valid value. If we manage to do that, the set of possible values is drastically decreased, and we eliminate the possibility of using the DayOfWeek component of a Result in which the InputError component is NoInput or Invalid (figure 3.5).

图 3.5。type的所有可能值作为orResult的组合。那是 9 个值 (2 + 7 )。我们不再需要 OK ,因为我们有一个值这一事实表明没有错误。 InputError DayOfWeekInputErrorDayOfWeekInputErrorDayOfWeek

自己动手做

一个Either类型包含两种类型,TLeftTRight,约定是TLeft存储错误类型和TRight存储有效值类型。(如果没有错误,则值为“正确”。)同样,一些编程语言将此作为其库的一部分提供,但如果需要,我们可以轻松实现这样的类型。

An Either type wraps two types, TLeft and TRight, the convention being that TLeft stores the error type and TRight stores the valid value type. (If there’s no error, the value is “right”.) Again, some programming languages provide this as part of their library, but if necessary, we can easily implement such a type.

清单 3.14。Either类型
类 Either<TLeft, TRight> {
    私有只读值:TLeft | 对;                      1
    个私有只读左:布尔值;                              1个

    私有构造函数(值:TLeft | TRight,左:布尔值){   2
        this.value = 值;
        this.left = 左;
    }

    isLeft(): 布尔值 {
        返回 this.left;
    }

    getLeft(): TLeft {                                            3
        如果(!this.isLeft())抛出新的错误();

        返回 <TLeft>this.value;
    }

    isRight(): 布尔值 {
        返回 !this.left;
    }

    getRight(): TRight {                                          3
        如果(!this.isRight())抛出新的错误();

        返回 <TRight>this.value;
    }

    static makeLeft<TLeft, TRight>(value: TLeft) {                4
        返回新的 Either<TLeft, TRight>(value, true);
    }

    static makeRight<TLeft, TRight>(value: TRight) {              4
        返回新的 Either<TLeft, TRight>(value, false);
    }
}
class Either<TLeft, TRight> {
    private readonly value: TLeft | TRight;                      1
    private readonly left: boolean;                              1

    private constructor(value: TLeft | TRight, left: boolean) {  2
        this.value = value;
        this.left = left;
    }

    isLeft(): boolean {
        return this.left;
    }

    getLeft(): TLeft {                                           3
        if (!this.isLeft()) throw new Error();

        return <TLeft>this.value;
    }

    isRight(): boolean {
        return !this.left;
    }

    getRight(): TRight {                                         3
        if (!this.isRight()) throw new Error();

        return <TRight>this.value;
    }

    static makeLeft<TLeft, TRight>(value: TLeft) {               4
        return new Either<TLeft, TRight>(value, true);
    }

    static makeRight<TLeft, TRight>(value: TRight) {             4
        return new Either<TLeft, TRight>(value, false);
    }
}

  • 1 该类型包装了 TLeft 或 TRight 的值和一个标志以跟踪使用的是哪种类型。
  • 1 The type wraps a value of TLeft or TRight and a flag to keep track of which type is used.
  • 2 私有构造函数,因为我们需要确保值和布尔标志同步
  • 2 Private constructor, as we need to make sure that the value and boolean flag are in sync
  • 3 当我们有一个 TRight 时试图获得一个 TLeft,反之亦然会抛出一个错误。
  • 3 Attempting to get a TLeft when we have a TRight or vice versa throws an error.
  • 4 工厂函数调用构造函数并确保布尔标志与值一致。
  • 4 Factory functions call the constructor and ensure that the boolean flag is consistent with the value.

在缺少类型运算符 | 的语言中,我们可以简单地将值设为通用类型,例如Object在 Java 和 C# 中。andgetLeft()方法getRight()处理返回到TLeftandTRight类型的转换。

In a language that’s missing the type operator |, we could simply make the value a common type, such as Object in Java and C#. The getLeft() and getRight() methods handle conversion back to the TLeft and TRight types.

使用这样的类型,我们可以更新我们的parseDayOfWeek()实现以返回Either<InputError, DayOfWeek>结果并使其无法传播无效或默认DayOfWeek值。如果该函数返回一个,则结果中InputError没有,并且尝试通过调用 解包一个抛出错误。 DayOfWeekgetLeft()

With such a type, we can update our parseDayOfWeek() implementation to return an Either<InputError, DayOfWeek> result and make it impossible to propagate an invalid or default DayOfWeek value. If the function returns an InputError, there is no DayOfWeek in the result, and attempting to unwrap one via a call to getLeft() throws an error.

同样,我们必须明确解包值。当我们知道我们有一个有效值(isLeft()returns true),并且我们用 提取它时getLeft(),我们保证有有效数据。

Again, we have to be explicit about unpacking the value. When we know that we have a valid value (isLeft() returns true), and we extract it with getLeft(), we are guaranteed to have valid data.

清单 3.15。从函数返回结果或错误
枚举输入错误 {                                             1
    没有输入,
    无效的
}

类型 Result = Either<InputError, DayOfWeek>;                  2个

函数 parseDayOfWeek(输入:字符串):结果 {
    如果(输入==“”)
        返回 Either.makeLeft(InputError.NoInput);          3个

    开关(输入。toLowerCase()){                            3
        案例“星期天”:
            返回 Either.makeRight(DayOfWeek.Sunday);
        案例“星期一”:
            返回 Either.makeRight(DayOfWeek.Monday);
        案例“星期二”:
            返回 Either.makeRight(DayOfWeek.Tuesday);
        案例“星期三”:
            返回 Either.makeRight(DayOfWeek.Wednesday);
        案例“星期四”:
            返回 Either.makeRight(DayOfWeek.Thursday);
        案例“星期五”:
            返回 Either.makeRight(DayOfWeek.Friday);
        案例“星期六”:
            返回 Either.makeRight(DayOfWeek.Saturday);
        默认:
            返回 Either.makeLeft(InputError.Invalid);      3个
    }
}
enum InputError {                                            1
    NoInput,
    Invalid
}

type Result = Either<InputError, DayOfWeek>;                 2

function parseDayOfWeek(input: string): Result {
    if (input == "")
        return Either.makeLeft(InputError.NoInput);          3

    switch (input.toLowerCase()) {                           3
        case "sunday":
            return Either.makeRight(DayOfWeek.Sunday);
        case "monday":
            return Either.makeRight(DayOfWeek.Monday);
        case "tuesday":
            return Either.makeRight(DayOfWeek.Tuesday);
        case "wednesday":
            return Either.makeRight(DayOfWeek.Wednesday);
        case "thursday":
            return Either.makeRight(DayOfWeek.Thursday);
        case "friday":
            return Either.makeRight(DayOfWeek.Friday);
        case "saturday":
            return Either.makeRight(DayOfWeek.Saturday);
        default:
            return Either.makeLeft(InputError.Invalid);      3
    }
}

  • 1 我们不再需要 OK InputError。如果我们没有错误,我们就有一个值。
  • 1 We no longer need an OK InputError. If we don’t have an error, we have a value.
  • 2 我们将 Result 更新为 InputError 或 DayOfWeek 而不是两者的组合。
  • 2 We update Result to be an InputError or a DayOfWeek instead of a combination of the two.
  • 3 我们通过 Either.makeRight 和 Either.makeLeft 返回结果或错误。
  • 3 We return a result or an error by Either.makeRight and Either.makeLeft.

更新后的实现利用类型系统来消除无效状态,例如(NoInput, Sunday)我们可能不小心使用了该Sunday值。此外,也不需要为OK值,InputError因为如果解析成功,我们就不会出现错误。

The updated implementation leverages the type system to eliminate invalid states such as (NoInput, Sunday) from which we could’ve accidentally used the Sunday value. Also, there’s no need for an OK value for InputError because we don’t have an error if parsing succeeds.

例外情况

错误抛出异常是结果或错误的一个完全有效的例子:函数要么返回结果,要么抛出异常。在某些情况下,不能使用异常,而是Either首选类型,例如在跨进程或跨线程传播错误时;作为设计原则,当错误本身并不例外时(通常是我们处理用户输入的情况);调用使用错误代码的操作系统 API 时;等等。在这些情况下,当我们不能或不想抛出异常但需要传达我们得到一个值或失败时,最好将其编码为值或错误,而不是错误

Throwing an exception on error is a perfectly valid example of result or error: the function either returns a result or throws an exception. In several situations, exceptions cannot be used and an Either type is preferred, such as when propagating errors across processes or across threads; as a design principle, when the error itself is not exceptional (often the case when we deal with user input); when calling operating system APIs that use error codes; and so on. In these situations, when we can’t or don’t want to throw an exception but need to communicate that we got a value or failed, it’s best to encode this as an either value or error as opposed to value and error.

当抛出异常是可接受的时,我们可以将它们用作另一种方式来确保我们不会以无效结果 错误结束。当抛出异常时,该函数不再通过使用语句将值传回给调用者来返回“正常”方式returncatch相反,它会传播异常对象,直到找到匹配为止。这样,我们得到一个结果或异常。我们不会深入讨论抛出异常,因为尽管许多语言都提供了抛出和捕获异常的功能,但从类型的角度来看,异常并不是很特别。

When throwing exceptions is acceptable, we can use them as another way to ensure that we don’t end up with an invalid result and an error. When an exception is thrown, the function no longer returns the “normal” way, by passing back a value to the caller with a return statement. Rather, it propagates the exception object until a matching catch is found. This way, we get a result or an exception. We won’t cover throwing exceptions in depth, because although many languages provide facilities for exceptions to be thrown and caught, from a type perspective, exceptions aren’t very special.

3.2.4.变体

3.2.4. Variants

我们已经了解了可选类型,它包含基础类型的值或没有值。然后我们查看了either包含 aTLeftTRight值的类型。这些类型的概括是变体类型

We’ve looked at optional types, which contain a value of the underlying type or no value. Then we looked at either types, which contain a TLeft or a TRight value. The generalizations of these types are the variant types.

变异类型

变体类型,也称为标记联合类型,包含任意数量的基础类型的值。标记来自这样一个事实,即即使底层类型具有重叠的值,我们仍然能够准确地分辨出该值来自哪个类型。

Variant types, also known as tagged union types, contain a value of any number of underlying types. Tagged comes from the fact that even if the underlying types have overlapping values, we are still able to tell exactly which type the value comes from.

让我们看一下清单 3.16中几何形状集合的示例。每个形状都有一组不同的属性和一个标签(作为kind属性实现)。我们可以定义一个类型,它是所有这些形状的联合。然后,当我们想要(例如)渲染这些形状时,我们可以使用它们的kind属性来确定实例是哪些可能的形状,然后将其转换为该形状。此过程与前面示例中的展开相同。

Let’s look at an example of a collection of geometric shapes in listing 3.16. Each shape has a different set of properties and a tag (implemented as a kind property). We can define a type that is the union of all these shapes. Then, when we want to (for example) render these shapes, we can use their kind property to determine which of the possible shapes an instance is, then cast it to that shape. This process is the same as the unwrapping in previous examples.

清单 3.16。标记的形状并集
类点{
    只读种类:字符串=“点”;
    x: 数字 = 0;
    y: 数字 = 0;
}

类圈子{
    只读类型:string = "Circle";
    x: 数字 = 0;
    y: 数字 = 0;
    半径:数字= 0;
}

类矩形{
    只读种类:字符串=“矩形”;
    x: 数字 = 0;
    y: 数字 = 0;
    宽度:数字 = 0;
    高度:数字= 0;
}

输入形状 = 点 | 圈子 | 长方形;

让形状:Shape[] = [new Circle(), new Rectangle()];

for (let shape of shapes) {                                        1 
    switch (shape.kind) {                                          1
        案例“点”:
            让点:Point = <Point>shape;                      2个
            console.log(`Point ${JSON.stringify(point)}`);
            休息;
        案例“圆”:
            let circle: Circle = <Circle>shape;                   2个
            console.log(`Circle ${JSON.stringify(circle)}`);
            休息;
        案例“矩形”:
            let rectangle: Rectangle = <Rectangle>shape;          2个
            console.log(`矩形 ${JSON.stringify(rectangle)}`);
            休息;
        默认值:                                                   3
            抛出新的错误();
    }
}
class Point {
    readonly kind: string = "Point";
    x: number = 0;
    y: number = 0;
}

class Circle {
    readonly kind: string = "Circle";
    x: number = 0;
    y: number = 0;
    radius: number = 0;
}

class Rectangle {
    readonly kind: string = "Rectangle";
    x: number = 0;
    y: number = 0;
    width: number = 0;
    height: number = 0;
}

type Shape = Point | Circle | Rectangle;

let shapes: Shape[] = [new Circle(), new Rectangle()];

for (let shape of shapes) {                                       1
    switch (shape.kind) {                                         1
        case "Point":
            let point: Point = <Point>shape;                      2
            console.log(`Point ${JSON.stringify(point)}`);
            break;
        case "Circle":
            let circle: Circle = <Circle>shape;                   2
            console.log(`Circle ${JSON.stringify(circle)}`);
            break;
        case "Rectangle":
            let rectangle: Rectangle = <Rectangle>shape;          2
            console.log(`Rectangle ${JSON.stringify(rectangle)}`);
            break;
        default:                                                  3
            throw new Error();
    }
}

  • 1 我们遍历形状并检查每个形状的种类属性。
  • 1 We iterate over the shapes and check the kind property of each.
  • 2 如果 kind 是“Point”,我们可以安全地将形状用作 Point。圆形和矩形也是如此。
  • 2 If the kind is “Point”, we can safely use the shape as a Point. The same is true for Circle and Rectangle.
  • 3 如果种类未知,我们会抛出一个错误。这意味着某种其他类型以某种方式进入了联盟,而这种情况永远不会发生。
  • 3 We throw an error if the kind is unknown. This means that some other type somehow made its way into the union, which should never be the case.

在前面的示例中,kind每个类的成员代表告诉我们值的实际类型的标记。的值shape.kind告诉我们 Shape 实例是PointCircle还是Rectangle。我们还可以实现一个通用的变体来跟踪类型,而不需要类型本身存储标签。

In the preceding example, the kind member of each class represents the tag which tells us the actual type of a value. The value of shape.kind tells us whether the Shape instance is a Point, Circle, or Rectangle. We can also implement a general--purpose variant that keeps track of the types without requiring the types themselves to store a tag.

让我们实现一个简单的变体,它最多可以存储三种类型的值,并根据类型索引跟踪存储的实际类型。

Let’s implement a simple variant that can store a value of up to three types and keep track of the actual type stored based on a type index.

DIY变体

不同的编程语言提供不同的通用和类型检查功能。例如,某些语言允许可变数量的泛型参数(因此我们可以拥有任意数量类型的变体);其他人提供了不同的方法来在编译和运行时确定一个值是否属于某种类型。

Different programming languages provide different generic and type-checking features. Some languages allow a variable number of generic arguments, for example (so we can have variants of any number of types); others provide different ways to determine whether a value is of a certain type at both compile and run time.

以下 TypeScript 实现有一些不一定可以转换为其他编程语言的权衡。它是通用变体的起点,但它会以不同的方式实现,例如,Java 或 C#。例如,TypeScript 不支持方法重载,但在其他语言中,我们可以在每个泛型类型上重载一个make()函数。

The following TypeScript implementation has some trade-offs that don’t necessarily translate to other programming languages. It’s a starting point for a general--purpose variant, but it would be implemented differently in, say, Java or C#. TypeScript doesn’t support method overloads, for example, but in other languages, we could get away with a single make() function overloaded on each generic type.

清单 3.17。Variant类型
类变体<T1, T2, T3> {
    只读值:T1 | T2 | T3;
    只读索引:数字;

    私有构造函数(值:T1 | T2 | T3,索引:数字){
        this.value = 值;
        this.index = 索引;
    }

    static make1<T1, T2, T3>(值:T1):变体<T1, T2, T3> {
        返回新变量<T1, T2, T3>(值, 0);
    }

    static make2<T1, T2, T3>(值:T2):变体<T1,T2,T3> {
        返回新变量<T1, T2, T3>(值, 1);
    }

    static make3<T1, T2, T3>(值: T3): 变体<T1, T2, T3> {
        返回新变量<T1, T2, T3>(值, 2);
    }
}
class Variant<T1, T2, T3> {
    readonly value: T1 | T2 | T3;
    readonly index: number;

    private constructor(value: T1 | T2 | T3, index: number) {
        this.value = value;
        this.index = index;
    }

    static make1<T1, T2, T3>(value: T1): Variant<T1, T2, T3> {
        return new Variant<T1, T2, T3>(value, 0);
    }

    static make2<T1, T2, T3>(value: T2): Variant<T1, T2, T3> {
        return new Variant<T1, T2, T3>(value, 1);
    }

    static make3<T1, T2, T3>(value: T3): Variant<T1, T2, T3> {
        return new Variant<T1, T2, T3>(value, 2);
    }
}

此实现负责维护标签,因此现在我们可以将它们从我们的几何形状中删除。

This implementation takes on the responsibility of maintaining the tags, so now we can remove them from our geometric shapes.

清单 3.18。形状联合作为变体
类点{                                        1
    x: 数字 = 0;
    y: 数字 = 0;
}

类圆{                                       1
    x: 数字 = 0;
    y: 数字 = 0;
    半径:数字= 0;
}

矩形类 {                                    1
    x: 数字 = 0;
    y: 数字 = 0;
    宽度:数字 = 0;
    高度:数字= 0;
}

type Shape = Variant<Point, Circle, Rectangle> ;     2个

让形状:Shape[] = [
    Variant.make2(new Circle()),
    Variant.make3(新矩形())
];

对于(让形状的形状){
    开关(shape.index){                           3
        案例 0:
            让点:Point = <Point>shape.value;  3个
            console.log(`Point ${JSON.stringify(point)}`);
            休息;
        情况1:
            let circle: Circle = <Circle>shape.value;
            console.log(`Circle ${JSON.stringify(circle)}`);
            休息;
        案例 2:
            let rectangle: Rectangle = <Rectangle>shape.value;
            console.log(`矩形 ${JSON.stringify(rectangle)}`);
            休息;
        默认:
            抛出新的错误();
    }
}
class Point {                                       1
    x: number = 0;
    y: number = 0;
}

class Circle {                                      1
    x: number = 0;
    y: number = 0;
    radius: number = 0;
}

class Rectangle {                                   1
    x: number = 0;
    y: number = 0;
    width: number = 0;
    height: number = 0;
}

type Shape = Variant<Point, Circle, Rectangle>;     2

let shapes: Shape[] = [
    Variant.make2(new Circle()),
    Variant.make3(new Rectangle())
];

for (let shape of shapes) {
    switch (shape.index) {                          3
        case 0:
            let point: Point = <Point>shape.value;  3
            console.log(`Point ${JSON.stringify(point)}`);
            break;
        case 1:
            let circle: Circle = <Circle>shape.value;
            console.log(`Circle ${JSON.stringify(circle)}`);
            break;
        case 2:
            let rectangle: Rectangle = <Rectangle>shape.value;
            console.log(`Rectangle ${JSON.stringify(rectangle)}`);
            break;
        default:
            throw new Error();
    }
}

  • 1 形状不再需要自己存储标签。
  • 1 Shapes no longer need to store tags themselves.
  • 2 Shape 现在是这三种类型的 Variant。
  • 2 Shape is now a Variant of these three types.
  • 3 我们查看索引属性以找到标签,查看值属性以获取实际对象。
  • 3 We look at the index property to find the tag and the value property to get the actual object.

这个实现可能看起来并没有增加很多好处;我们最终使用数字标签并任意决定 0 是 aPoint和 1 是 a Circle。您可能还想知道为什么我们不为我们的形状使用类层次结构,我们有一个基本方法,每个类型都实现而不是切换标签。

This implementation might not look as though it adds a lot of benefit; we ended up using numeric tags and arbitrarily decided that 0 is a Point and 1 is a Circle. You might also wonder why we didn’t use a class hierarchy for our shapes, where we have a base method that each type implements instead of switching on tags.

对于该任务,我们需要了解访问者设计模式及其实现方式。

For that task, we need to take a look at the visitor design pattern and the ways in which it can be implemented.

3.2.5.练习

3.2.5. Exercises

1个

用户可以在红色、绿色和蓝色中进行选择。这个选择的类型应该是什么?

  1. numberRed = 0, Green = 1,Blue = 2
  2. stringRed = "Red", Green = "Green",Blue = "Blue"
  3. enum Colors { Red, Green, Blue }
  4. type Colors = Red | Green | Blue颜色是类别

1

Users can provide a selection among the colors red, green, and blue. What should be the type of this selection?

  1. number with Red = 0, Green = 1, Blue = 2
  2. string with Red = "Red", Green = "Green", Blue = "Blue"
  3. enum Colors { Red, Green, Blue }
  4. type Colors = Red | Green | Blue where the colors are classes

2个

将字符串作为输入并将其解析为数字的函数的返回类型应该是什么?该函数不会抛出。

  1. number
  2. number | undefined
  3. Optional<number>
  4. b或c

2

What should be the return type of a function that takes a string as input and parses it into a number? The function does not throw.

  1. number
  2. number | undefined
  3. Optional<number>
  4. Either b or c

3个

操作系统通常使用数字来表示错误代码。可以返回数值或数字错误代码的函数的返回类型应该是什么?

  1. number
  2. { value: number, error: number }
  3. number | number
  4. Either<number, number>

3

Operating systems usually use numbers to represent error codes. What should be the return type of a function that can return either a numerical value or a numerical error code?

  1. number
  2. { value: number, error: number }
  3. number | number
  4. Either<number, number>

3.3. 访客模式

3.3. The visitor pattern

让我们回顾一下访问者设计模式,看看遍历构成文档的项目——首先通过面向对象的镜头,然后使用我们实现的通用标记联合类型。如果您对访问者设计模式不是很熟悉,请不要担心;我们将在完成示例时回顾它的工作原理。

Let’s go over the visitor design pattern and look at traversing the items that make up a document—first through an object-oriented lens and then with the generic tagged union type we implemented. Don’t worry if you aren’t very familiar with the visitor design pattern; we’ll review how it works as we’re working through our example.

我们将从一个朴素的实现开始,展示访问者设计模式如何改进设计,然后展示一个替代的实现,它消除了类层次结构的需要。

We’ll start with a naïve implementation, show how the visitor design pattern improves the design, and then show an alternative implementation that removes the need for class hierarchies.

我们从三个文档项目开始:段落、图片和表格。我们希望将它们呈现在屏幕上,或者让屏幕阅读器为视障用户大声朗读它们。

We start with three document items: paragraph, picture, and table. We want to either render them onscreen or have a screen reader read them aloud for visually impaired users.

3.3.1.天真的实现

3.3.1. A naïve implementation

我们可以采取的一种方法是提供一个通用接口,以确保每个项目都知道如何在屏幕上绘制自己并读取自己,如下一个清单所示。

One approach we can take is to provide a common interface to ensure that each item knows how to draw itself on a screen and read itself, as shown in the next listing.

清单 3.19。天真的实施
class Renderer { /* 渲染方法 */ }              1 
class ScreenReader { /* 屏幕阅读方法 */ }     1

接口 IDocumentItem {                               2
    渲染(渲染器:渲染器):无效;
    阅读(屏幕阅读器:屏幕阅读器):无效;
}

类段落实现 IDocumentItem {              3
    /* 段落成员省略 */
    渲染(渲染器:渲染器){
        /* 使用渲染器在屏幕上绘制自己 */
    }

    阅读(屏幕阅读器:屏幕阅读器){
        /* 使用 screenReader 读取自身 */
    }
}

类图片实现 IDocumentItem {                3
    /* 省略图片成员 */
    渲染(渲染器:渲染器){
        /* 使用渲染器在屏幕上绘制自己 */
    }

    阅读(屏幕阅读器:屏幕阅读器){
        /* 使用 screenReader 读取自身 */
    }
}

类表实现 IDocumentItem {                  3
    /* 省略表成员 */
    渲染(渲染器:渲染器){
        /* 使用渲染器在屏幕上绘制自己 */
    }

    阅读(屏幕阅读器:屏幕阅读器){
        /* 使用 screenReader 读取自身 */
    }
}

let doc: IDocumentItem[] = [new Paragraph(), new Table()];
让渲染器:渲染器=新渲染器();

for (let item of doc) {
    item.render(渲染器);
}
class Renderer { /* Rendering methods */ }             1
class ScreenReader { /* Screen reading methods */ }    1

interface IDocumentItem {                              2
    render(renderer: Renderer): void;
    read(screenReader: ScreenReader): void;
}

class Paragraph implements IDocumentItem {             3
    /* Paragraph members omitted */
    render(renderer: Renderer) {
        /* Uses renderer to draw itself on screen */
    }

    read(screenReader: ScreenReader) {
        /* Uses screenReader to read itself */
    }
}

class Picture implements IDocumentItem {               3
    /* Picture members omitted */
    render(renderer: Renderer) {
        /* Uses renderer to draw itself on screen */
    }

    read(screenReader: ScreenReader) {
        /* Uses screenReader to read itself */
    }
}

class Table implements IDocumentItem {                 3
    /* Table members omitted */
    render(renderer: Renderer) {
        /* Uses renderer to draw itself on screen */
    }

    read(screenReader: ScreenReader) {
        /* Uses screenReader to read itself */
    }
}

let doc: IDocumentItem[] = [new Paragraph(), new Table()];
let renderer: Renderer = new Renderer();

for (let item of doc) {
    item.render(renderer);
}

  • 1 这两个类提供了渲染和读取的方法,这里为了简洁省略。
  • 1 The two classes provide methods to render and read, omitted here for brevity.
  • 2 IDocumentItem 接口指定每个项目都可以呈现自己并读取自己。
  • 2 The IDocumentItem interface specifies that each item can render itself and read itself.
  • 3 文档元素实现 IDocumentItem,并在给定渲染器或屏幕阅读器的情况下自行绘制或大声朗读。
  • 3 Document elements implement IDocumentItem and, given a renderer or screen reader, draw themselves or read themselves aloud.

从设计的角度来看,这种方法并不好。文档项存储描述文档内容的信息,例如文本或图像,不应该负责其他事情,例如呈现和可访问性。在每个文档项类中包含呈现和可访问性代码会使代码膨胀。更糟糕的是,如果我们需要添加新功能(例如,用于打印),我们需要更新接口和所有实现类来实现新功能。

This approach is not great from a design point of view. The document items store information that describes document content, such as text or an image, and should not be responsible for other things, such as rendering and accessibility. Having rendering and accessibility code in each document item class bloats the code. Worse, if we need to add a new capability—say, for printing—we need to update the interface and all implementing classes to implement the new capability.

3.3.2.使用访问者模式

3.3.2. Using the visitor pattern

访问者模式是对对象结构的元素执行的操作。此模式允许您定义一个新操作,而无需更改它所操作的元素的类。

The visitor pattern is an operation to be performed on elements of an object structure. This pattern lets you define a new operation without changing the classes of the elements on which it operates.

在清单 3.20所示的示例中,该模式应该允许我们添加新功能而无需触及文档项的代码。我们可以使用双分派机制来完成这项任务,其中文档项接受任何访问者,然后将自己传递给它。访问者知道如何处理每个单独的项目(通过呈现、大声朗读等等),因此给定项目的实例,它会执行正确的操作(图 3.6

In our example shown in listing 3.20, the pattern should allow us to add a new capability without having to touch the code of the document items. We can achieve this task with the double-dispatch mechanism, in which document items accept any visitor and then pass themselves to it. The visitor knows how to process each individual item (by rendering it, reading it aloud, and so on), so given an instance of the item, it performs the right operation (figure 3.6).

图 3.6。访客模式。该IDocumentItem接口确保每个文档项都有一个accept()采用IVisitor. IVisitor确保每个访问者都能处理所有可能的文档项目类型。每个文档项都执行accept()将其自身发送给访问者。使用这种模式,我们可以将职责(例如屏幕呈现和可访问性)分离到各个组件(访问者),并将它们从文档项中抽象出来。

双重分派来自这样一个事实,即给定一个,首先调用IDocumentItem正确的方法;accept()然后,给定IVisitor参数,执行正确的操作。

Double dispatch comes from the fact that, given an IDocumentItem, the right accept() method is called first; then, given the IVisitor argument, the right operation is performed.

清单 3.20。用访客模式处理
接口 IVisitor {                                      1
    访问段落(段落:段落):无效;
    访问图片(图片:图片):void;
    访问表(表:表):无效;
}

类渲染器实现 IVisitor {                      2
    访问段落(段落:段落){ /* ... */ }
    visitPicture(picture: 图片) { /* ... */ }
    visitTable(table: Table) { /* ... */ }
}

类 ScreenReader 实现 IVisitor {                  1
    访问段落(段落:段落){ /* ... */ }
    visitPicture(picture: 图片) { /* ... */ }
    visitTable(table: Table) { /* ... */ }
}

接口 IDocumentItem {                                 3
    接受(访客:IVisitor):无效;
}

类段落实现 IDocumentItem {
    /* 段落成员省略 */
    接受(访客:IVisitor){
        visitor.visitParagraph(这个);                    4个
    }
}

类图片实现 IDocumentItem {
    /* 省略图片成员 */
    接受(访客:IVisitor){
        visitor.visitPicture(这个);                      4个
    }
}

类表实现 IDocumentItem {
    /* 省略表成员 */
    接受(访客:IVisitor){
        visitor.visitTable(这个);                        4个
    }
}

let doc: IDocumentItem[] = [new Paragraph(), new Table()];
让渲染器:IVisitor = new Renderer();

for (let item of doc) {
    item.accept(渲染器);
}
interface IVisitor {                                     1
    visitParagraph(paragraph: Paragraph): void;
    visitPicture(picture: Picture): void;
    visitTable(table: Table): void;
}

class Renderer implements IVisitor {                     2
    visitParagraph(paragraph: Paragraph) { /* ... */ }
    visitPicture(picture: Picture) { /* ... */ }
    visitTable(table: Table) { /* ... */ }
}

class ScreenReader implements IVisitor {                 1
    visitParagraph(paragraph: Paragraph) { /* ... */ }
    visitPicture(picture: Picture) { /* ... */ }
    visitTable(table: Table) { /* ... */ }
}

interface IDocumentItem {                                3
    accept(visitor: IVisitor): void;
}

class Paragraph implements IDocumentItem {
    /* Paragraph members omitted */
    accept(visitor: IVisitor) {
        visitor.visitParagraph(this);                    4
    }
}

class Picture implements IDocumentItem {
    /* Picture members omitted */
    accept(visitor: IVisitor) {
        visitor.visitPicture(this);                      4
    }
}

class Table implements IDocumentItem {
    /* Table members omitted */
    accept(visitor: IVisitor) {
        visitor.visitTable(this);                        4
    }
}

let doc: IDocumentItem[] = [new Paragraph(), new Table()];
let renderer: IVisitor = new Renderer();

for (let item of doc) {
    item.accept(renderer);
}

  • 1 IVisitor 接口指定每个访问者应该能够处理所有形状。
  • 1 The IVisitor interface specifies that each visitor should be able to process all shapes.
  • 2 具体的Renderer和ScreenReader实现了这个接口。
  • 2 The concrete Renderer and ScreenReader implement this interface.
  • 3 现在文档项只需要实现接受任何访问者的 accept() 方法。
  • 3 Now document items need only implement an accept() method that takes any visitor.
  • 4 项对访问者调用适当的方法并将它们自己作为参数传递。
  • 4 Items call the appropriate method on the visitor and pass themselves as arguments.

现在访问者可以遍历一组IDocumentItem对象并通过调用accept()每个对象来处理它们。处理的责任从物品本身转移到访客身上。添加新访客不会影响文档项;新的访问者只需要实现IVisitor接口,文档项就会像接受其他任何东西一样接受它。

Now a visitor can go over a collection of IDocumentItem objects and process them by calling accept() on each. The responsibility of processing is moved from the items themselves to the visitors. Adding a new visitor does not affect the document items; the new visitor just needs to implement the IVisitor interface, and document items would accept it as they would any other.

新的访问者类将在、和方法Printer中实现打印段落、图片和表格的逻辑。文档项目本身无需更改即可打印。 visitParagraph()visitPicture()visitTable()

A new Printer visitor class would implement logic to print a paragraph, a picture, and a table in the visitParagraph(), visitPicture(), and visitTable() methods. The document items themselves would become printable without having to change.

这个例子是访问者模式的经典实现。接下来,让我们看看如何通过使用变体来实现类似的效果。

This example is a classical implementation of the visitor pattern. Next, let’s look at how we could achieve something similar by using a variant instead.

3.3.3.访问变体

3.3.3. Visiting a variant

首先,让我们回到我们的通用变体类型并实现一个visit()函数,该函数接受一个变体和一组函数,每个类型一个,并且(取决于存储在变体中的值)将正确的函数应用于它。

First, let’s go back to our generic variant type and implement a visit() function that takes a variant and a set of functions, one for each type, and (depending on the value stored in the variant) applies the right function to it.

清单 3.21。变异访客
函数访问<T1, T2, T3>(
    变体:变体<T1,T2,T3>,
    func1: (value: T1) => void,                       1 
    func2: (value: T2) => void,                       1 
    func3: (value: T3) => void                        1
): 空白 {
    开关(变体索引){
        案例 0: func1(<T1>variant.value); 休息;     2
        案例 1:func2(<T2>variant.value); 休息;     2
        案例 2:func3(<T3>variant.value); 休息;     2个
        默认值:抛出新的错误();
    }
}
function visit<T1, T2, T3>(
    variant: Variant<T1, T2, T3>,
    func1: (value: T1) => void,                      1
    func2: (value: T2) => void,                      1
    func3: (value: T3) => void                       1
): void {
    switch (variant.index) {
        case 0: func1(<T1>variant.value); break;     2
        case 1: func2(<T2>variant.value); break;     2
        case 2: func3(<T3>variant.value); break;     2
        default: throw new Error();
    }
}

  • 1 访问函数将构成变体的每种类型的函数作为参数。
  • 1 The visit function takes as arguments a function for each type that makes up the variant.
  • 2 根据索引,调用匹配存储值类型的函数。
  • 2 Based on index, the function matching the type of the stored value is called.

如果我们将文档项放在变体中,我们可以使用此函数来选择适当的访问者方法。如果我们这样做,我们就不再需要强制我们的任何类实现某些接口:将正确的文档项与正确的处理方法相匹配的责任转移到这个通用函数中visit()

If we place our document items in a variant, we can use this function to select the appropriate visitor method. If we do this, we no longer have to force any of our classes to implement certain interfaces: responsibility for matching the right document item with the right processing method is moved to this generic visit() function.

文档项不再需要了解有关访问者的任何信息,也不需要“接受”他们,如以下清单所示。

Document items no longer need to know anything about visitors and don’t need to “accept” them, as the following listing shows.

清单 3.22。变体访问者的替代处理
类渲染器{
    渲染段落(段落:段落){ /* ... */ }
    渲染图片(图片:图片){ /* ... */ }
    renderTable(table: Table) { /* ... */ }
}

类屏幕阅读器{
    readParagraph(段落: 段落) { /* ... */ }
    readPicture(图片:图片) { /* ... */ }
    readTable(table: Table) { /* ... */ }
}

类段落 {                                     1
    /* 段落成员省略 */
}

类图片 {                                       1
    /* 省略图片成员 */
}

类表 {                                         1
    /* 省略表成员 */
}

let doc: Variant<段落, 图片, 表格>[] = [     2
    Variant.make1(新段落()),
    Variant.make3(新表())
];

让渲染器:渲染器=新渲染器();

for (let item of doc) {
    访问(项目,                                       3
        (段落:段落)=> renderer.renderParagraph(段落),
        (图片: 图片) => renderer.renderPicture(图片),
        (表:表)=> renderer.renderTable(表)
    );
}
class Renderer {
    renderParagraph(paragraph: Paragraph) { /* ... */ }
    renderPicture(picture: Picture) { /* ... */ }
    renderTable(table: Table) { /* ... */ }
}

class ScreenReader {
    readParagraph(paragraph: Paragraph) { /* ... */ }
    readPicture(picture: Picture) { /* ... */ }
    readTable(table: Table) { /* ... */ }
}

class Paragraph {                                    1
    /* Paragraph members omitted */
}

class Picture {                                      1
    /* Picture members omitted */
}

class Table {                                        1
    /* Table members omitted */
}

let doc: Variant<Paragraph, Picture, Table>[] = [    2
    Variant.make1(new Paragraph()),
    Variant.make3(new Table())
];

let renderer: Renderer = new Renderer();

for (let item of doc) {
    visit(item,                                      3
        (paragraph: Paragraph) => renderer.renderParagraph(paragraph),
        (picture: Picture) => renderer.renderPicture(picture),
        (table: Table) => renderer.renderTable(table)
    );
}

  • 1 文档项不再需要通用接口。
  • 1 Document items no longer need a common interface.
  • 2 我们将文档项目存储在可以容纳任何可用项目的变体中。
  • 2 We store document items in a variant that can hold any of the available items.
  • 3 访问功能为项目匹配正确的处理方法。
  • 3 The visit function matches the item with the right processing method.

通过这种方法,我们将双重分派机制与我们正在使用的类型分离,并将其移至变体/访问者。变体和访问者是可以跨不同问题域重用的通用类型。这种方法的优点是它让访问者只负责处理,文档项只负责存储域数据(图 3.7)。

With this approach, we decouple the double-dispatch mechanism from the types we are using and move it to the variant/visitor. The variant and visitor are generic types that can be reused across different problem domains. The advantage of this approach is that it lets visitors be responsible only for processing and document items be responsible only for storing domain data (figure 3.7).

图 3.7。简化的访问者模式:现在文档项和访问者不需要实现任何接口。将此图与图 3.6进行对比。将文档项与正确的访问者方法匹配的责任封装在该visit()方法中。从图中我们可以看出,类型是不相关的,这是一件好事:它使我们的程序更加灵活。

我们介绍的函数visit()也是使用变体类型的预期方式。当我们想要准确地找出变体包含的类型时,对变体的索引执行切换可能很容易出错。但是通常,一旦我们有了一个变体,我们就不想提取它的价值;相反,我们通过使用visit(). 这样容易出错的switch在visit()实现中就处理好了,我们就不用操心了。将容易出错的代码封装在可重用的组件中是降低风险的好做法,因为当实现稳定并经过测试时,我们可以在多个场景中依赖它。

The visit() function we introduced is also the expected way to use a variant type. Performing a switch on the index of the variant when we want to figure out exactly which type it contains could be error-prone. But usually, once we have a variant, we don’t want to extract the value; instead, we apply functions to it by using visit(). This way, the error-prone switch is handled in the visit() implementation, and we don’t have to worry about it. Encapsulating error-prone code in a reusable component is good practice for reducing risk, because when the implementation is stable and tested, we can rely on it in multiple scenarios.

使用基于变体的访问者而不是经典的 OOP 实现的优点是它将我们的领域对象与访问者完全分开。现在我们甚至不需要accept()方法,文档项也不需要知道是什么在处理它们。它们也不必符合任何特定的接口,例如IDocumentItem在我们的示例中。这是因为将访问者与形状匹配的胶水代码封装在Variant及其visit()功能中。

Using a variant-based visitor instead of the classical OOP implementation has the advantage that it fully separates our domain objects from the visitors. Now we don’t even need an accept() method, and document items don’t need to know anything about what is processing them. They also don’t have to conform to any particular interface, such as IDocumentItem in our example. That’s because the glue code that matches visitors with shapes is encapsulated in Variant and its visit() function.

3.3.4.锻炼

3.3.4. Exercise

1个

我们的visit()实施返回void。扩展它,以便给定 a ,它通过应用以下三个函数之一Variant<T1, T2, T3>返回 a : , or , or 。 Variant<U1, U2, U3>(value: T1) => U1(value: T2) => U2(value: T3) => U3

1

Our visit() implementation returns void. Extend it so that given a Variant<T1, T2, T3>, it returns a Variant<U1, U2, U3> by applying one of three functions: (value: T1) => U1, or (value: T2) => U2, or (value: T3) => U3.

3.4. 代数数据类型

3.4. Algebraic data types

您可能听说过代数数据类型(ADT)一词。ADT 是在类型系统中组合类型的方法。事实上,这正是我们在本章中所涵盖的内容。ADT 提供了两种组合类型的方法:乘积类型和求和类型。

You might have heard the term algebraic data types (ADTs). ADTs are ways to combine types within a type system. In fact, this is exactly what we covered during this chapter. ADTs provide two ways to combine types: product types and sum types.

3.4.1.产品类型

3.4.1. Product types

产品类型就是我们在本章中所说的复合类型。元组和记录是产品类型,因为它们的值是它们的组合类型的产品。类型 A = {a1, a2}(type Awith possible values a1and a2) 和B = {b1, b2}(type Bwith possible values b1and b2) 组合成元组类型<A, B>作为A x B = {(a1, b1), (a1, b2), (a2, b1), (a2, b2)}.

Product types are what we called compound types in this chapter. Tuples and records are product types because their values are products of their composing types. The types A = {a1, a2} (type A with possible values a1 and a2) and B = {b1, b2} (type B with possible values b1 and b2) combine into the tuple type <A, B> as A x B = {(a1, b1), (a1, b2), (a2, b1), (a2, b2)}.

产品类型

产品类型将多个其他类型组合成一个新类型,该新类型存储每个组合类型的值。A类型、B产品类型C——我们可以写成A x B x C——包含一个来自 的值A、一个来自 的值B和一个来自 的值。元组和记录类型是产品类型的示例。此外,记录允许我们为它们的每个组件分配有意义的名称。 C

Product types combine multiple other types into a new type that stores a value from each of the combined types. The product type of types A, B, and C—which we can write as A x B x C—contains a value from A, a value from B, and a value from C. Tuple and record types are examples of product types. Additionally, records allow us to assign meaningful names to each of their components.

记录类型应该非常熟悉,因为它们通常是新程序员学习的第一个组合方法。最近,元组已进入主流编程语言,但它们应该不会特别难理解。元组与记录类型非常相似,除了我们不能命名它们的成员并且通常可以通过指定构成元组的类型来内联定义它们。例如,在 TypeScript 中,[number, number]定义了由两个值组成的元组类型number

Record types should be very familiar, as they are usually the first composition method that new programmers learn. Recently, tuples have made their way into mainstream programming languages, but they shouldn’t be particularly hard to understand. Tuples are very similar to record types except that we can’t name their members and usually can define them inline by specifying the types that make up the tuple. In TypeScript, for example, [number, number] defines the tuple type composed of two number values.

我们在求和类型之前介绍了产品类型,因为它们应该更熟悉。几乎所有的编程语言都提供了定义记录类型的方法。较少的主流语言为求和类型提供句法支持。

We covered product types before sum types, as they should be more familiar. Almost all programming languages provide ways to define record types. Fewer mainstream languages provide syntactic support for sum types.

3.4.2.求和类型

3.4.2. Sum types

Sum 类型就是我们在本章前面所说的either-or类型。它们通过允许来自任何一种类型的值来组合类型,但只能是其中一种。类型A = {a1, a2}B = {b1, b2}合并为总和类型A | Bas A + B = {a1, a2, b1, b2}

Sum types are what we called either-or types earlier in this chapter. They combine types by allowing a value from any one of the types, but only one of them. The types A = {a1, a2} and B = {b1, b2} combine into the sum type A | B as A + B = {a1, a2, b1, b2}.

求和类型

Sum 类型将多个其他类型组合成一个新类型,该新类型存储来自任何一种组合类型的值。类型AB和的总和类型C——我们可以写成——A + B + C包含一个来自 的值A一个来自 的值B一个来自 的值C。可选类型和变体类型是总和类型的示例。

Sum types combine multiple other types into a new type that stores a value from any one of the combined types. The sum type of types A, B, and C—which we can write as A + B + C—contains a value from A, or a value from B, or a value from C. Optional and variant types are examples of sum types.

正如我们所见,TypeScript 有 | 类型运算符,但可以在没有它的情况下实现常见的求和类型,例如Optional,Either和。Variant这些类型提供了强大的方法来表示结果或错误以及类型的封闭集,并启用了不同的方法来实现常见的访问者模式。

As we saw, TypeScript has the | type operator, but common sum types such as Optional , Either, and Variant can be implemented without it. These types provide powerful ways for representing result or error and closed sets of types, and enable different ways to implement the common visitor pattern.

通常,总和类型允许我们将不相关类型的值存储在单个变量中。与访问者模式示例一样,面向对象的替代方案是使用公共基类或接口,但扩展性不佳。如果我们在应用程序的不同地方混合和匹配不同的类型,我们最终会得到很多接口或基类,这些接口或基类并不是特别可重用。Sum 类型提供了一种简单、干净的方法来为此类场景组合类型。

In general, sum types allow us to store values from unrelated types in a single variable. As in the visitor pattern example, an object-oriented alternative would be to use a common base class or interface, but that doesn’t scale as well. If we mix and match different types in different places of our application, we end up with a lot of interfaces or base classes that aren’t particularly reusable. Sum types provide a simple, clean way to compose types for such scenarios.

3.4.3.练习

3.4.3. Exercises

1个

以下语句声明了哪种类型?

让 x: [数字, 字符串] = [42, "你好"];

  1. 原始类型
  2. 额头型
  3. 产品类型
  4. 金额和产品类型

1

What kind of type does the following statement declare?

let x: [number, string] = [42, "Hello"];

  1. A primitive type
  2. A sum type
  3. A product type
  4. Both a sum and a product type

2个

以下语句声明了哪种类型?

让 y: 数字 | string = "你好";

  1. 原始类型
  2. 额头型
  3. 产品类型
  4. 金额和产品类型

2

What kind of type does the following statement declare?

let y: number | string = "Hello";

  1. A primitive type
  2. A sum type
  3. A product type
  4. Both a sum and a product type

3个

给定 anenum Two { A, B }和 an enum Three { C, D, E },元组类型[Two, Three]有多少个可能的值?

  1. 2个
  2. 5个
  3. 6个
  4. 8个

3

Given an enum Two { A, B } and an enum Three { C, D, E }, how many possible values does the tuple type [Two, Three] have?

  1. 2
  2. 5
  3. 6
  4. 8

4个

给定 anenum Two { A, B }和 an enum Three { C, D, E },该类型有多少个可能的值Two | Three

  1. 2个
  2. 5个
  3. 6个
  4. 8个

4

Given an enum Two { A, B } and an enum Three { C, D, E }, how many possible values does the type Two | Three have?

  1. 2
  2. 5
  3. 6
  4. 8

概括

Summary

  • 产品类型是对来自多种类型的值进行分组的元组和记录。
  • Product types are tuples and records that group values from multiple types.
  • 记录使我们能够命名成员,从而赋予他们意义。记录比元组留下更少的歧义空间。
  • Records allow us to name members, thus giving them meaning. Records leave less room for ambiguity than tuples.
  • 不变量是格式良好的记录必须遵守的规则。如果一个类型有不变量,创建成员privatereadonly确保不变量被强制执行并且外部代码不能破坏它们。
  • Invariants are rules that a well-formed record must obey. If a type has invariants, making members private or readonly ensures that the invariants are enforced and that external code cannot break them.
  • Sum 类型将类型分组为非此即彼,其中值属于其中一种组件类型。
  • Sum types group types as either-or, in which values are of one of the component types.
  • 函数应该返回一个值一个错误,而不是一个值一个错误。
  • Functions should return a value or an error, not a value and an error.
  • 可选类型持有基础类型的值或什么都没有。当值的缺失本身不是变量域的一部分时(null十亿美元的错误),它通常不太容易出错。
  • Optional types hold a value of the underlying type or nothing. It’s generally less error-prone when the absence of a value is not itself part of the domain of a variable (null billion-dollar mistake).
  • 两种类型都持有左类型或右类型的值。按照惯例,正确的就是正确的,所以左的就是错误的。
  • Either types hold a value of the left or the right type. By convention, right is right, so left is error.
  • 变体可以包含任意数量的基础类型的值,并使我们能够表达一组封闭类型的值,而不需要它们之间有任何关系(没有公共接口或基类型)。
  • Variants can hold a value of any number of underlying types and enable us to express values of a closed sets of types without requiring any relationship between them (no common interfaces or base type).
  • 将正确的函数应用于变体的访问者函数可以实现访问者模式的替代实现,并更好地划分职责。
  • A visitor function that applies the right function to a variant enables an alternative implementation of the visitor pattern, with better division of responsibilities.

在本章中,我们介绍了通过组合现有类型来创建新类型的各种方法。在第 4 章中,我们将看到如何通过依赖类型系统来编码含义和限制类型的允许值范围来提高程序的安全性。我们还将了解如何添加和删除类型信息,以及如何将其应用于序列化等场景。

In this chapter, we covered various ways to create new types by combining existing types. In chapter 4, we’ll see how we can increase the safety of our program by relying on the type system to encode meaning and restricting the range of allowed values for our types. We’ll also see how we can add and remove type information and how this can be applied to scenarios such as serialization.

习题答案

Answers to exercises

复合类型

Compound types

1个

c—命名坐标的三个分量是首选方法。

1

c—Naming the three components of the coordinates is the preferred approach.

 

 

用类型表达 either-or

Expressing either-or with types

1个

c—枚举在这种情况下是合适的。根据现有要求,不需要课程。

1

c—An enum is appropriate in this case. With existing requirements, classes aren’t needed.

2个

d——要么是内置求和类型,要么Optional是有效的返回类型,因为两者都可以表示没有值

2

d—Either a built-in sum type or Optional is a valid return type, as both can represent the absence of a value

3个

d—最好是区分联合类型(number | number无法区分该值是否表示错误。)

3

d—A discriminate union type is best (number | number wouldn’t be able to distinguish whether the value represents an error.)

 

 

访客模式

The visitor pattern

1个

这是一个可能的实现:

函数访问<T1, T2, T3, U1, U2, U3>(
    变体:变体<T1,T2,T3>,
    func1: (值: T1) => U1,
    func2: (值: T2) => U2,
    func3:(值:T3)=> U3
): 变体<U1, U2, U3> {
    开关(变体索引){
       案例 0:
            返回 Variant.make1(func1(<T1>variant.value));
        情况1:
            返回 Variant.make2(func2(<T2>variant.value));
        案例 2:
            返回 Variant.make3(func3(<T3>variant.value));
        默认值:抛出新的错误();
    }
}

1

Here is a possible implementation:

function visit<T1, T2, T3, U1, U2, U3>(
    variant: Variant<T1, T2, T3>,
    func1: (value: T1) => U1,
    func2: (value: T2) => U2,
    func3: (value: T3) => U3
): Variant<U1, U2, U3> {
    switch (variant.index) {
       case 0:
            return Variant.make1(func1(<T1>variant.value));
        case 1:
            return Variant.make2(func2(<T2>variant.value));
        case 2:
            return Variant.make3(func3(<T3>variant.value));
        default: throw new Error();
    }
}

 

 

代数数据类型

Algebraic data types

1个

c—元组是产品类型。

1

c—Tuples are product types.

2个

b—这是一个 TypeScript 和类型。

2

b—This is a TypeScript sum type.

3个

c—因为元组是产品类型,所以我们将两个枚举 ( 2x 3) 的可能值相乘。

3

c—Because tuples are product types, we multiply the possible values of the two enums (2 x 3).

4个

2b—因为这是求和类型,所以我们将两个枚举 ( + )的可能值相加3

4

b—Because this is a sum type, we add the possible values of the two enums (2 + 3).

 

 

第 4 章。类型安全

Chapter 4. Type safety

本章涵盖

This chapter covers

  • 避免原始的痴迷反模式
  • Avoiding the primitive obsession antipattern
  • 在实例构造期间强制执行约束
  • Enforcing constraints during instance construction
  • 通过添加类型信息提高安全性
  • Increasing safety by adding type information
  • 通过隐藏和恢复类型信息来提高灵活性
  • Increasing flexibility by hiding and restoring type information

现在我们知道了如何使用我们的编程语言提供的基本类型以及如何组合它们来创建新类型,让我们看看如何通过使用类型使我们的程序更安全。更安全,我的意思是减少错误的机会。

Now that we know how to use the basic types provided by our programming language and how to compose them to create new types, let’s look at how we can make our programs safer by using types. By safer, I mean reducing the opportunity for bugs.

有几种方法可以通过创建编码附加信息的新类型来实现这一点:意义和保证。前者(我们将在第一部分介绍)消除了我们误解值的机会,例如将一英里误认为一公里。0后者允许我们在类型系统中编码保证,例如“此类型的实例永远不会小于 ”。这两种技术都使我们的代码更安全,因为我们从可能值集中消除了无效值 由类型表示,并尽快避免误解,最好是在编译时,或者如果是在运行时,则在我们实例化我们的类型时尽快。当我们有一个类型的实例时,从那时起我们就知道它代表什么并且它是一个有效值。

There are a couple of ways to achieve this by creating new types that encode additional information: meanings and guarantees. The former, which we’ll cover in the first section, removes the opportunity for us to misinterpret a value, such as mistaking a mile for a kilometer. The latter allows us to encode guarantees such as “an instance of this type will never be less than 0” in the type system. Both techniques make our code safer, as we eliminate invalid values from the set of possible values represented by a type and avoid misunderstandings as soon as we can, preferably at compile time or as soon as we instantiate our types if at run time. When we have an instance of one of our types, from then on we know what it represents and that it is a valid value.

因为我们正在讨论类型安全,所以我们还将研究如何手动添加和隐藏类型检查器的信息。如果我们以某种方式比类型检查器知道的更多,我们可以告诉它信任我们并将我们的信息传递给它。另一方面,如果类型检查器知道太多并最终阻碍了我们的工作,我们可以让它“忘记”一些类型信息,以安全为代价为我们提供更大的灵活性。这些技术不能轻易使用,因为它们将正确类型检查的责任从类型检查器转移到我们作为开发人员,但正如我们将看到的,有一些合法的场景需要这些技术。

Because we’re discussing type safety, we’ll also look at how we can add and hide information from the type checker manually. If we somehow know more than the type checker does, we can tell it to trust us and pass our information down to it. On the other hand, if the type checker knows too much and ends up impeding our work, we can make it “forget” some of the typing information, giving us more flexibility at the cost of safety. These techniques are not to be used lightly, as they move the responsibility of proper type checking from the type checker to us as developers, but as we’ll see, there are some legitimate scenarios in which these techniques are desired.

4.1. 避免原始痴迷以防止误解

4.1. Avoiding primitive obsession to prevent misinterpretation

在本节中,我们将看到当代码的两个不同部分(通常由不同的开发人员编写)做出不兼容的假设时,如何使用基本类型来表示值并隐式假设这些值表示什么会导致问题(图 4.1

In this section, we’ll see how using basic types to represent values and implicitly assuming what those values represent can cause problems when two different parts of the code, often written by different developers, make incompatible assumptions (figure 4.1).

图 4.1。数值1000可以表示 1,000 美元或 1,000 英里。两个不同的开发人员可以将其解释为两种截然不同的措施。

我们可以依靠类型系统通过定义类型来描述它们来明确这些假设,在这种情况下,类型检查器可以检测到不兼容性并在任何不良事件发生之前发出信号。

We can rely on the type system to make those assumptions explicit by defining types to describe them, in which case the type checker can detect incompatibilities and signal them before anything bad happens.

假设我们有一个函数addToBill(),它的参数是 a number。该功能应该将商品的价格添加到账单中。因为参数是 类型的number,我们可以将城市之间的距离以英里为单位传递给它,也表示为 a number。我们最终将里程数加到总价格中,类型检查器不会怀疑任何事情!

Let’s say we have a function addToBill() that takes as its argument a number. The function is supposed to add the price of an item to a bill. Because the argument is of type number, we could pass it a distance between cities in miles, also represented as a number. We end up adding miles to a price total, and the type checker doesn’t suspect anything!

图 4.2。具有显式Currency类型可以清楚地表明该值不代表 1,000 英里,而是代表美元金额。

另一方面,如果我们让我们的addToBill()函数接受 type 的参数Currency并且我们的城市之间的距离表示为 type Miles,代码将无法编译(图 4.2)。

On the other hand, if we make our addToBill() function take an argument of type Currency and our distance between cities is represented as a type Miles, the code will not compile (figure 4.2).

4.1.1.火星气候轨道器

4.1.1. The Mars Climate Orbiter

火星气候轨道飞行器解体是因为洛克希德公司开发的组件使用不同的动量测量单位(磅力秒)而不是 NASA 开发的组件,后者使用该测量值(公制单位)。让我们想象一下代码如何查找这两个组件。该trajectory-Correction()函数以牛顿秒或 Ns(动量的公制单位)为单位消耗测量值,而该provideMomentum()函数以磅力秒或 lbfs 为单位产生测量值,如下一个清单所示。

The Mars Climate Orbiter disintegrated because a component developed by Lockheed used a different unit of measure (pound-force seconds) for momentum than a component developed by NASA, which consumed that measure (in metric units). Let’s imagine how the code looked for the two components. The trajectory-Correction() function consumes a measurement as Newton-seconds, or Ns (the metric unit for momentum), whereas the provideMomentum() function produces a measure in pound-force seconds, or lbfs, as shown in the next listing.

清单 4.1。不兼容组件的草图
function trajectoryCorrection(momentum: number) {     1 
    if (momentum < 2 /* Ns */) {                     2
        瓦解();
    }

    /* ... */
}

函数 provideMomentum() {
    trajectoryCorrection(1.5 /* lbfs */);            3 
}
function trajectoryCorrection(momentum: number) {    1
    if (momentum < 2 /* Ns */)  {                    2
        disintegrate();
    }

    /* ... */
}

function provideMomentum() {
    trajectoryCorrection(1.5 /* lbfs */);            3
}

  • 1 trajectoryCorrection 以 momentum 作为 number 类型的参数。
  • 1 trajectoryCorrection takes momentum as an argument of type number.
  • 2 如果动量小于 2 Ns,则解体。
  • 2 If momentum is less than 2 Ns, disintegrate.
  • 3 provideMomentum 通过 1.5 lbfs 的测量值。
  • 3 provideMomentum passes in a measurement of 1.5 lbfs.

转换为公制,1 lbfs 等于 4.448222 Ns。从功能的角度来看provide-Momentum(),提供的值很好,因为 1.5 lbfs 比 6 Ns 多。这远远超过了 2 Ns 的下限。什么地方出了错?这种情况下的主要问题是,两个分量都将动量视为一个数字,隐含地假定了测量动量的单位。trajectoryCorrection()将动量解释为 1 Ns,小于 2 Ns 下限,不恰当地触发了解体。

Converting to metric, 1 lbfs equals 4.448222 Ns. From the perspective of the provide-Momentum() function, the value provided is good, because 1.5 lbfs is more than 6 Ns. That’s way more than the 2 Ns lower limit. What went wrong? The main issue in this case is that both components treated momentum as a number, implicitly assuming the unit in which it was measured. trajectoryCorrection() interpreted the momentum as 1 Ns, less than the 2 Ns lower limit, and inappropriately triggered the disintegration.

让我们看看我们是否可以利用类型系统来防止这种灾难性的误解。让我们通过在清单 4.2Lbfs中定义类型和类型来明确度量单位。两种类型都包含一个数字,因为实际度量仍然是一个值。我们将为每种类型使用唯一的符号,因为 TypeScript 认为具有相同形状的类型是兼容的,正如我们将在讨论子类型时看到的那样。独特的符号技巧使得一种类型不能被隐式解释为另一种类型。并非所有语言都需要这个额外的唯一符号成员。我们将在第 7 章解释这个技巧;现在,我们将专注于定义的新类型。 Ns

Let’s see whether we can leverage the type system to prevent such catastrophic misunderstandings. Let’s make the unit of measure explicit by defining a Lbfs type and a Ns type in listing 4.2. Both types wrap a number, as the actual measure is still a value. We will use a unique symbol for each type because TypeScript considers types to be compatible if they have the same shape, as we will see when we discuss subtyping. The unique symbol trick makes it so that one type can’t be implicitly interpreted as the other. Not all languages require this additional unique symbol member. We’ll explain this trick in chapter 7; for now, we’ll focus on the new types defined.

清单 4.2。磅力秒和牛顿秒类型
声明 const NsType:唯一符号;         1个

类 Ns {
   只读值:数字;                   2 
    [NsType]: void;                          1个

    构造函数(值:数字){
        this.value = 值;
    }
}

声明 const LbfsType:唯一符号;

类 Lbfs {
    只读值:数字;                  3 
    [LbfsType]: void;                        3个

    构造函数(值:数字){
        this.value = 值;
    }
}
declare const NsType: unique symbol;         1

class Ns {
   readonly value: number;                   2
    [NsType]: void;                          1

    constructor(value: number) {
        this.value = value;
    }
}

declare const LbfsType: unique symbol;

class Lbfs {
    readonly value: number;                  3
    [LbfsType]: void;                        3

    constructor(value: number) {
        this.value = value;
    }
}

  • 1 TypeScript 特定的方式来确保具有相同形状的其他对象不能被解释为这种类型
  • 1 TypeScript-specific way to ensure that other objects with the same shape can’t be interpreted as this type
  • 2 Ns 实际上只是包装了一个数字类型的值。
  • 2 Ns effectively just wraps a value of type number.
  • 3 同样,Lbfs 类型包装了一个数字和一个唯一符号。
  • 3 Similarly, Lbfs type wraps a number and a unique symbol.

现在我们有了两个不同的类型,我们可以很容易地实现它们之间的转换,因为我们知道比率。让我们看看下面的清单,看看从 lbfs 到 Ns 的转换,这是我们更新trajectoryCorrection()代码中需要的。

Now that we have our two separate types, we can easily implement a conversion between them because we know the ratio. Let’s look at the following listing to see a conversion from lbfs to Ns, which we need in our update trajectoryCorrection() code.

清单 4.3。将 lbfs 转换为 Ns
函数 lbfsToNs(lbfs: Lbfs): Ns {
    返回新的 Ns(lbfs.value * 4.448222);     1 
}
function lbfsToNs(lbfs: Lbfs): Ns {
    return new Ns(lbfs.value * 4.448222);     1
}

  • 1 取 lbfs 值,乘以比率,返回 Ns 值。
  • 1 Take the lbfs value, multiply by the ratio, and return a Ns value.

回到火星气候轨道器,我们可以重新实现这两个函数以使用新类型。trajectoryCorrection()期望 Ns 动量(如果该值小于 2 Ns,仍会分解),并且provideMomentum()仍会产生 lbfs 值。但是现在我们不能简单地获取由产生的值provideMomentum()并将其传递给trajectoryCorrection(),因为返回值和函数参数具有不同的类型。我们必须使用我们的lbfsToNs()函数显式地从一个转换为另一个,如以下清单所示。

Going back to the Mars Climate Orbiter, we can reimplement the two functions to use the new types. trajectoryCorrection() expects a Ns momentum (and will still disintegrate if the value is less than 2 Ns), and provideMomentum() still produces values as lbfs. But now we can’t simply take the value produced by provideMomentum() and pass it to trajectoryCorrection(), because the returned value and the function argument have different types. We have to explicitly convert from one to the other, using our lbfsToNs() function, as the following listing shows.

清单 4.4。更新的组件
function trajectoryCorrection(momentum: Ns ) {          1 
    if ( momentum.value < new Ns(2).value ) {           1
        瓦解();
    }

   /* ... */
}

函数 provideMomentum() {
    trajectoryCorrection( lbfsToNs(new Lbfs(1.5)) );    2 
}
function trajectoryCorrection(momentum: Ns) {         1
    if (momentum.value < new Ns(2).value)  {          1
        disintegrate();
    }

   /* ... */
}

function provideMomentum() {
    trajectoryCorrection(lbfsToNs(new Lbfs(1.5)));    2
}

  • 1 trajectoryCorrection 现在采用 Ns 类型的参数并将其与 2 Ns 进行比较。
  • 1 trajectoryCorrection now takes an argument of type Ns and compares it with 2 Ns.
  • 2 provideMomentum 生成一个 1.5 lbfs 值,必须将其转换为 Ns。
  • 2 provideMomentum generates a 1.5 lbfs value and has to convert it to Ns.

如果我们省略转换lbfsToNs(),代码将无法编译,并且会出现以下错误:Argument of type 'lbfs' is not assignable to parameter of type 'Ns'. Property '[NsType]' is missing in type 'lbfs'.

If we omitted the conversion lbfsToNs(), the code would simply not compile, and we would get the following error: Argument of type 'lbfs' is not assignable to parameter of type 'Ns'. Property '[NsType]' is missing in type 'lbfs'.

让我们回顾一下发生了什么:我们从两个都操纵动量值的组件开始,但即使它们在处理这些值时使用不同的单位,它们都将这些值简单地表示为number. 为了避免误解,我们创建了几个新类型,一个代表每个度量单位,这有效地消除了误解的余地。如果一个组件显式地处理Ns,它就不会意外地消耗一个Lbfs值。

Let’s review what happened: we started with two components that both manipulated momentum values, but even though they used different units when handling those values, they both represented the values simply as number. To avoid misinterpretations, we created a couple of new types, one to represent each unit of measure, which effectively left no room for misinterpretation. If a component explicitly deals with Ns, it can’t accidentally consume a Lbfs value.

另请注意,在我们的第一个示例 ( ) 中作为注释出现在代码中的假设1.5 /* lbfs */在我们的最终实现 ( new Lbfs(1.5)) 中变成了代码。

Also note that the assumptions that showed up in the code as comments in our first example (1.5 /* lbfs */) became code in our final implementation (new Lbfs(1.5)).

4.1.2.原始的痴迷反模式

4.1.2. The primitive obsession antipattern

与设计模式捕获高度可靠和有效的可重用软件设计的方式相同,反模式是常见的设计,当存在更好的替代方案时,这些设计无效且适得其反。前面的示例是一个名为primitive obsession的著名反模式的实例。当我们依赖基本类型来表示一切时,就会出现对原始的痴迷:邮政编码是 a number,电话号码是 a string,等等。

In the same way that design patterns capture reusable software designs that are highly reliable and effective, antipatterns are common designs that are ineffective and counterproductive when a better alternative exists. The preceding example is an instance of a well-known antipattern called primitive obsession. Primitive obsession turns up when we rely on basic types to represent everything: a postal code is a number, a phone number is a string, and so on.

如果我们落入这个陷阱,就会为我们在本节中看到的错误留出很大空间。那是因为值的含义没有在类型系统中明确捕获。如果我消耗一个作为 a 给出的动量值number,我,开发人员,隐含地假设它是一个牛顿秒值。类型检查器没有足够的信息来检测两个开发人员何时做出不兼容的假设。当此假设被显式捕获为类型声明时,并且我使用作为实例给出的动量值Ns时,类型检查器可以验证其他人何时试图给我一个Lbfs实例而不是允许代码编译。

If we fall into this trap, we leave a lot of room for errors like the one we saw in this section. That’s because the meaning of the values is not explicitly captured in the type system. If I consume a momentum value given as a number, I, the developer, implicitly assume that it is a Newton-second value. The type checker does not have enough information to detect when two developers make incompatible assumptions. When this assumption is explicitly captured as a type declaration, and I consume a momentum value given as a Ns instance, the type checker can verify when someone else is attempting to give me a Lbfs instance instead and not allow the code to compile.

尽管邮政编码是一个数字,但这并不意味着我们应该将它存储为 type 的值number。我们永远不应该将动量解释为邮政编码。

Even though a postal code is a number, that doesn’t mean we should store it as a value of type number. We should never interpret momentum as a postal code.

如果您表示的实体是简单的值,例如物理测量值和邮政编码,请考虑将它们定义为新类型,即使这些类型只是简单地包含一个数字或字符串。这种做法为类型系统提供了更多信息来分析我们的代码,并消除了由不兼容假设引起的一整类错误,更不用说它使代码更具可读性。作为对比,将 的第一个定义(trajectoryCorrection()即 )与第二个定义(即 )进行比较。第二个向代码的读者提供更多关于其合同是什么的信息。(预期动量在.trajectory-Correction(momentum: number)trajectory--Correction(momentum: Ns)Ns)

If the entities you represent are simple values, such as physical measurements and postal codes, consider defining them as new types, even if these types simply wrap a number or a string. This practice gives the type system more information to work with in analyzing our code and eliminates a whole class of errors caused by incompatible assumptions, not to mention that it makes the code more readable. For contrast, compare the first definition of trajectoryCorrection(), which is trajectory-Correction(momentum: number), with the second one, which is trajectory--Correction(momentum: Ns). The second one gives more information to readers of the code as to what its contract is. (Expected momentum is in Ns.)

到目前为止,我们已经了解了如何将基本类型包装到其他类型中以编码更多信息。现在让我们继续看看如何通过限制给定类型的允许值范围来提供更高的安全性。

So far, we’ve seen how we can wrap primitive types into other types to encode more information. Now let’s move on to see how we can provide even more safety by restricting the range of allowed values for a given type.

4.1.3.锻炼

4.1.3. Exercise

1个

表示重量测量的最安全方法是什么?

  1. 作为一个number
  2. 作为一个string
  3. 作为自定义Kilograms类型
  4. 作为自定义Weight类型

1

What is the safest way to represent a weight measurement?

  1. As a number
  2. As a string
  3. As a custom Kilograms type
  4. As a custom Weight type

4.2. 强制约束

4.2. Enforcing constraints

第 3 章中,我们讨论了组合以及如何采用基本类型并将它们组合起来以表示更复杂的概念,例如将 2D 平面上的点表示为一对数值,X 和 Y 坐标各一个。现在让我们看看当开箱即用的基本类型允许比我们需要的更多的值时我们可以做什么。

In chapter 3, we talked about composition and how to take basic types and combine them to represent more complex concepts, such as representing a point on a 2D plane as a pair of number values, one for each of the X and Y coordinates. Now let’s look at what we can do when the basic types we get out of the box allow for more values than we need.

让我们以测量温度为例。我们将避免原始的痴迷并声明一个Celsius类型以明确我们期望温度具有哪种测量单位。这种类型也将简单地包装一个数字。

Let’s take, as an example, a measure of temperature. We’re going to avoid primitive obsession and declare a Celsius type to make it clear which unit of measure we expect the temperature to have. This type will also simply wrap a number.

不过,我们还有一个额外的限制条件:我们的温度绝不能低于绝对零值,即 –273.15 摄氏度。一种选择是在我们使用这种类型的实例时检查该值是否有效。但是,此选项为错误留下了空间:我们总是添加检查,但团队中的新开发人员不知道该模式并且错过了检查。确保我们永远不会得到无效值不是更好吗?

We have an additional constraint, though: we should never have a temperature less than absolute zero, which is –273.15 degrees Celsius. One option is to check whenever we use an instance of this type that the value is a valid one. This option leaves room for error, though: we always add the check, but a new developer on the team doesn’t know the pattern and misses checking. Wouldn’t it be better to make sure that we can never get an invalid value?

我们可以通过两种方式做到这一点:通过构造函数或通过工厂。

We can do this in two ways: via the constructor or via a factory.

4.2.1.使用构造函数强制约束

4.2.1. Enforcing constraints with the constructor

我们可以在构造函数中实现约束,并以我们在查看整数溢出时看到的两种方式之一来处理太小的值。一种选择是在值无效时抛出异常并禁止创建对象。

We can implement the constraint in the constructor and handle a value that’s too small in one of the two ways we saw when we looked at integer overflow. One option is to throw an exception when the value is invalid and disallow creation of the object.

清单 4.5。构造函数抛出无效值
声明 const celsiusType:唯一符号;

类摄氏{
    只读值:数字;                       1个
    [摄氏类型]: void;

   构造函数(值:数字){
        如果(值 < -273.15)抛出新错误();   2个

        this.value = 值;
    }
}
declare const celsiusType: unique symbol;

class Celsius {
    readonly value: number;                       1
    [celsiusType]: void;

   constructor(value: number) {
        if (value < -273.15) throw new Error();   2

        this.value = value;
    }
}

  • 1 该值是不可变的,所以当它被初始化时,它不能被改变。
  • 1 The value is immutable, so when it’s initialized, it can’t be changed.
  • 2 如果我们尝试创建无效温度,构造函数将抛出。
  • 2 Constructor throws if we attempt to create an invalid temperature.

我们通过 make 确保值在构造后保持有效readonly。另一种选择是将其设为私有并使用 getter 访问它(以便可以检索但不能设置值)。

We ensure that the value stays valid after construction by making it readonly. Another option would be to make it private and access it with a getter (so that the value can be retrieved but not set).

我们还可以实现我们的构造函数以将值强制为有效值:任何小于-273.15becomes 的值-273.15

We can also implement our constructor to coerce the value to be a valid one: anything less than -273.15 becomes -273.15.

清单 4.6。构造函数强制无效值
声明 const celsiusType:唯一符号;

类摄氏{
    只读值:数字;
    [摄氏类型]: void;

    构造函数(值:数字){
        如果(值<-273.15)值=-273.151个

        this.value = 值;
    }
}
declare const celsiusType: unique symbol;

class Celsius {
    readonly value: number;
    [celsiusType]: void;

    constructor(value: number) {
        if (value < -273.15) value = -273.15;     1

        this.value = value;
    }
}

  • 1 我们不是抛出,而是“固定”值。
  • 1 Instead of throwing, we “fix” the value.

这两种方法中的任何一种都有效,具体取决于场景。我们也可以改用工厂函数。工厂是一个类或函数其主要工作是创建另一个对象。

Either of the two approaches is valid, depending on the scenario. We can also use a factory function instead. A factory is a class or function whose main job is to create another object.

4.2.2.对工厂实施约束

4.2.2. Enforcing constraints with a factory

当我们不想抛出异常,而是返回undefined或不是温度的其他值并表示无法创建有效实例时,工厂很有用。构造函数不能这样做,因为它不返回:它要么完成实例初始化,要么抛出异常。使用工厂的另一个原因是当构造和验证对象所需的逻辑很复杂时,在这种情况下,在构造函数之外实现它可能是有意义的。根据经验,构造函数不应该做繁重的工作——只需初始化对象成员即可。

A factory is useful when we don’t want to throw an exception, but to return undefined or some other value that is not a temperature and represents failure to create a valid instance. A constructor can’t do this because it doesn’t return: it either finishes initializing its instance or throws. Another reason to use a factory is when the logic required to construct and validate an object is complex, in which case it might make sense to implement it outside the constructor. As a rule of thumb, constructors shouldn’t do heavy lifting—just get the object members initialized.

让我们看看下面清单中工厂的实现是如何工作的。我们将使构造函数成为私有的,这样只有工厂方法才能调用它。工厂将是我们班级的静态方法。它将返回一个Celsius实例或undefined.

Let’s look at how an implementation of a factory works in the following listing. We will make the constructor private so that only the factory method can call it. The factory will be a static method on our class. It will return either a Celsius instance or undefined.

清单 4.7。工厂返回undefined无效值
声明 const celsiusType:唯一符号;

类摄氏{
    只读值:数字;
    [摄氏类型]: void;

    私有构造函数(值:数字){                         1
        this.value = 值;
    }

    静态 makeCelsius(值:数字):摄氏 | 未定义 {     2
        如果(值 < -273.15)返回未定义;                  3个

        返回新的摄氏度(值);
    }
}
declare const celsiusType: unique symbol;

class Celsius {
    readonly value: number;
    [celsiusType]: void;

    private constructor(value: number) {                        1
        this.value = value;
    }

    static makeCelsius(value: number): Celsius | undefined {    2
        if (value < -273.15) return undefined;                  3

        return new Celsius(value);
    }
}

  • 1 构造函数现在是私有的,因为它本身不执行任何检查。
  • 1 Constructor is now private because it doesn’t perform any checks itself.
  • 2 工厂返回有效的 Celsius 实例或未定义。
  • 2 Factory returns either a valid Celsius instance or undefined.
  • 3 在工厂强制执行约束,这是创建 Celsius 实例的唯一方法。
  • 3 Constraint is enforced in the factory, which is the only way to create Celsius instances.

在所有这些情况下,我们都有额外的保证,如果我们有一个 的实例Celsius,它的值永远不会小于-273.15。在创建该类型的实例时执行检查并确保不能以其他方式创建该类型的优点是,只要您看到该类型的实例被传递,就可以保证您获得有效值。

In all these cases, we have the additional guarantee that if we have an instance of Celsius, its value will never be less than -273.15. The advantage of performing the check when an instance of the type is created and ensuring that the type can’t be created in other ways is that you are guaranteed a valid value whenever you see an instance of the type being passed around.

我们不是在使用实例时检查实例是否有效,这通常意味着在多个地方执行检查,而是只执行一次检查,并使该类型的无效对象不可能存在。

Instead of checking whether the instance is valid when using it, which usually means performing the check in multiple places, we perform the check just once and make it impossible for an invalid object of the type to exist.

Celsius当然,这种技术超越了简单的值包装器,例如 。我们可以确保Date根据年、月、日创建的对象是有效的,并且不允许像 6 月 31 日这样的日期。在许多情况下,我们可以使用的基本类型不允许我们直接施加我们想要的限制,在这种情况下,我们可以创建封装额外约束的类型,并保证它们不会存在无效值。

This technique goes beyond simple value wrappers like Celsius, of course. We can ensure that a Date object created from a year, a month, and a day is valid and disallow dates like June 31. There are many cases in which the basic types at our disposal don’t allow us to impose the restrictions we want directly, in which case we can create types that encapsulate additional constraints and provide the guarantee that they can’t exist with invalid values.

接下来,让我们看看如何在整个代码中添加和隐藏键入信息,以及这种做法何时有用。

Next, let’s look at how we can add and hide typing information throughout our code and when this practice is useful.

4.2.3.锻炼

4.2.3. Exercise

1个

实现一个Percentage表示 0 到 100 之间的值的类型。小于 0 的值应变为 0,大于 100 的值应变为 100。

1

Implement a Percentage type that represents a value between 0 and 100. Values smaller than 0 should become 0, and values larger than 100 should become 100.

4.3. 添加类型信息

4.3. Adding type information

尽管类型检查具有强大的理论基础,但所有编程语言都提供了捷径,使我们能够绕过类型检查并告诉编译器将值视为特定类型。我们实际上是在说:“相信我们;我们比你更了解这种类型的什么。” 这称为类型转换——您可能以前听过这个术语。

Although type checking has strong theoretical foundations, all programming languages provide shortcuts that allow us to bypass the type checks and tell the compiler to treat a value as a certain type. We are effectively saying, “Trust us; we know what this type is better than you do.” This is called a type cast—a term you might have heard before.

类型转换

类型转换将表达式的类型转换为另一种类型。每种编程语言都有自己的规则,关于哪些转换有效,哪些转换无效,哪些可以由编译器自动完成,哪些必须使用额外的代码来完成(图 4.3

A type cast converts the type of an expression to another type. Each programming language has its own rules about which conversions are valid and which are not, which can be done automatically by the compiler, and which must be done with additional code (figure 4.3).

图 4.3。通过转换,我们可以将 16 位有符号整数类型的值转换为 UTF-8 编码字符。

4.3.1.类型铸造

4.3.1. Type casting

显式类型转换是一种允许我们告诉编译器将值视为具有特定类型的类型的转换。在 TypeScript 中,我们通过在值前NewType添加或在值后 添加来进行强制转换。<NewType>as NewType

An explicit type cast is a cast that allows us to tell the compiler to treat a value as though it had a certain type. In TypeScript, we do a cast to NewType by adding <NewType> in front of the value or by adding as NewType after the value.

如果使用不当,这种技术可能会很危险:如果我们绕过类型检查器,如果我们试图将某个值用作不是它的值,则会出现运行时错误。例如,我可以将我的Bike,我可以ride(),转换为SportsCar,但我仍然无法drive()这样做,如以下清单所示。

This technique can be dangerous when misused: if we bypass the type checker, we get a run-time error if we attempt to use a value as something it is not. I can cast my Bike, which I can ride(), to a SportsCar, for example, but I still won’t be able to drive() it, as the following listing shows.

清单 4.8。导致运行时错误的类型转换
类自行车{
    ride(): void { /* ... */ }
}

跑车类 {
    驱动器():void { /* ... */ }
}

让我的自行车:自行车=新自行车();                                    1 
                                                                  1 
myBike.ride();                                                    1个

让 myPretendSportsCar: SportsCar = <SportsCar><unknown>myBike;   2个

myPretendSportsCar.drive();                                       3个
class Bike {
    ride(): void { /* ... */ }
}

class SportsCar {
    drive(): void { /* ... */ }
}

let myBike: Bike = new Bike();                                    1
                                                                  1
myBike.ride();                                                    1

let myPretendSportsCar: SportsCar = <SportsCar><unknown>myBike;   2

myPretendSportsCar.drive();                                       3

  • 1 myBike 被创建为 Bike 类型,因此我们可以在其上调用 ride()。
  • 1 myBike is created as type Bike, so we can call ride() on it.
  • 2 我们可以告诉编译器将其视为 SportsCar,我们将其分配给 myPretendSportsCar。
  • 2 We can tell the compiler to treat it as a SportsCar, which we assign to myPretendSportsCar.
  • 3 在 myPretendSportsCar 上调用 drive() 会导致运行时错误。
  • 3 Calling drive() on myPretendSportsCar causes a run-time error.

在这里,我们可以告诉类型检查器让我们假装我们有一个SportsCar,但这并不意味着我们实际上有一个。调用drive导致抛出以下异常:TypeError: myPretendSportsCar.drive is not a function

Here, we can tell the type checker to let us pretend that we have a SportsCar, but that doesn’t mean we actually have one. Calling drive results in the following exception being thrown: TypeError: myPretendSportsCar.drive is not a function.

我们必须myBike先转换为unknown类型,然后再转换为 a SportsCar,因为 TypeScript 编译器意识到 theBikeSportsCar类型不重叠。(其中一种类型的有效值永远不可能是另一种类型的有效值。)所以简单地调用<SportsCar>myBike仍然会导致错误。相反,我们首先说<unknown>myBike,它告诉编译器忘记 的类型myBike。然后我们可以说,“相信我们;这是一个SportsCar。但正如我们所见,这仍然会导致运行时错误。在其他语言中,它可能会导致崩溃。一般来说,这种情况是无效的。那么这什么时候有用呢?

We had to cast myBike first to the unknown type and then to a SportsCar because the TypeScript compiler realizes that the Bike and SportsCar types don’t overlap. (A valid value of one of the types can never be a valid value of the other.) So simply calling <SportsCar>myBike still causes an error. Instead, we first say <unknown>myBike, which tells the compiler to forget the type of myBike. Then we can say, “Trust us; it’s a SportsCar.” But as we saw, this still causes a run-time error. In other languages, it can cause a crash. In general, such a situation is not valid. So when would this be useful?

4.3.2.跟踪类型系统之外的类型

4.3.2. Tracking types outside the type system

有时,我们知道的比类型检查器还多。让我们回顾一下第 3 章Either的实现。它存储或类型的值,并且有一个标志跟踪该值是否为,如下一个清单所示。 TLeftTRightbooleanTLeft

Sometimes, we know more than the type checker. Let’s revisit the Either implementation from chapter 3. It stores a value of TLeft or TRight type, and a boolean flag keeps track of whether the value is TLeft, as shown in the next listing.

清单 4.9。重新审视Either实施
类 Either<TLeft, TRight> {
    私有只读值:TLeft | 对;               1
    个私有只读左:布尔值;                       2个

    私有构造函数(值:TLeft | TRight,左:布尔值){
        this.value = 值;
        this.left = 左;
    }

    isLeft(): 布尔值 {
        返回 this.left;
    }

    getLeft(): TLeft {
        如果(!this.isLeft())抛出新的错误();            3 
                                                          3
        返回 <TLeft>this.value;                         3个
    }

    isRight(): 布尔值 {
        返回 !this.left;
    }

    getRight(): 正确{
        如果(!this.isRight())抛出新的错误();

        返回 <TRight>this.value;
    }

    静态 makeLeft<TLeft, TRight>(值: TLeft) {
        返回新的 Either<TLeft, TRight>(value, true);    4个
    }

   静态 makeRight<TLeft, TRight>(值: TRight) {
        返回新的 Either<TLeft, TRight>(value, false);   4个
    }
}
class Either<TLeft, TRight> {
    private readonly value: TLeft | TRight;               1
    private readonly left: boolean;                       2

    private constructor(value: TLeft | TRight, left: boolean) {
        this.value = value;
        this.left = left;
    }

    isLeft(): boolean {
        return this.left;
    }

    getLeft(): TLeft {
        if (!this.isLeft()) throw new Error();            3
                                                          3
        return <TLeft>this.value;                         3
    }

    isRight(): boolean {
        return !this.left;
    }

    getRight(): TRight {
        if (!this.isRight()) throw new Error();

        return <TRight>this.value;
    }

    static makeLeft<TLeft, TRight>(value: TLeft) {
        return new Either<TLeft, TRight>(value, true);    4
    }

   static makeRight<TLeft, TRight>(value: TRight) {
        return new Either<TLeft, TRight>(value, false);   4
    }
}

  • 1 我们存储一个 TLeft 类型或 TRight 类型的值。
  • 1 We store a value of type TLeft or type TRight.
  • 2 我们通过使用 left 属性来跟踪它是否是 TLeft。
  • 2 We keep track of whether it is a TLeft or not by using the left property.
  • 3 当我们想要获得一个 TLeft 时,我们检查我们是否存储了正确的类型;然后我们转向 TLeft。
  • 3 When we want to get a TLeft, we check whether we are storing the right type; then we cast to TLeft.
  • 4 makeLeft工厂初始化left为true;makeRight 将其初始化为 false。
  • 4 The makeLeft factory initializes left to true; makeRight initializes it to false.

这允许我们将两种类型组合成一个总和类型,该类型可以表示其中任何一个的值。但是,如果我们仔细观察,我们存储的值的类型是TLeft | TRight。在我们分配它之后,类型检查器不再知道value我们实际存储的是 aTLeft还是 a TRight。从现在开始,它将考虑value成为两者之一。这是我们存储值时想要的,但在某些时候,我们想使用它。

This allows us to combine two types into a sum type that can represent a value from either of them. If we look closely, though, the value we are storing has type TLeft | TRight. After we assign it, the type checker no longer knows whether the actual value we stored was a TLeft or a TRight. From now on, it will consider value to be either of the two. This is what we want while storing the value, but at some point, we would like to use it.

编译器不允许我们将 type 的值传递TLeft | TRight给需要值的函数TLeft,因为如果我们的值实际上是TRight,我们就会有麻烦了。如果我们有一个三角形或一个正方形,我们不一定能通过三角形槽。有一个三角形穿过它会起作用。但是如果我们有一个正方形(图 4.4)呢?

The compiler will not allow us to pass a value of type TLeft | TRight to a function that expects a TLeft value, because if our value is in fact TRight, we are going to be in trouble. If we have a triangle or a square, we can’t necessarily pass that through a triangular slot. It would work to have a triangle to pass through it. But what if we have a square (figure 4.4)?

图 4.4。如果我们有一个三角形或一个正方形,我们不能确定我们拥有的实际形状是否会通过三角形槽。如果它是三角形会,但如果它是正方形则不会。

尝试做这样的事情会导致编译器错误,这很好。但是我们知道类型检查器不知道的事情:我们从设置值时就知道它是来自 aTLeft还是 a TRight。如果我们使用 创建我们的对象makeLeft(),我们设置lefttrue。如果我们使用 创建我们的对象makeRight(),我们设置leftfalse,如下一个清单所示。即使类型检查器忘记了,我们也会跟踪这个事实。

Trying to do something like this results in a compiler error, which is good. But we know something the type checker doesn’t: we know from when we set the value whether it came from a TLeft or a TRight. If we created our object by using makeLeft(), we set left to true. If we created our object by using makeRight(), we set left to false, as shown in the next listing. We are keeping track of this fact even if the type checker forgets.

清单 4.10。makeLeftmakeRight
类 Either<TLeft, TRight> {
    私有只读值:TLeft | 对;
    私有只读:布尔值;                        1个

    私有构造函数(值:TLeft | TRight,左:布尔值){
        this.value = 值;
        this.left = 左;                                  2个
    }


    /* ... */

   静态 makeLeft<TLeft, TRight>(值: TLeft) {
        返回新的 Either<TLeft, TRight>(value, true );     3个
    }

    静态 makeRight<TLeft, TRight>(值: TRight) {
        返回新的 Either<TLeft, TRight>(value, false );    3个
    }
}
class Either<TLeft, TRight> {
    private readonly value: TLeft | TRight;
    private readonly left: boolean;                        1

    private constructor(value: TLeft | TRight, left: boolean) {
        this.value = value;
        this.left = left;                                  2
    }


    /* ... */

   static makeLeft<TLeft, TRight>(value: TLeft) {
        return new Either<TLeft, TRight>(value, true);     3
    }

    static makeRight<TLeft, TRight>(value: TRight) {
        return new Either<TLeft, TRight>(value, false);    3
    }
}

  • 1 left 告诉我们是否正在存储 TLeft。
  • 1 left tells us whether we are storing a TLeft.
  • 2 left 在只有makeLeft 和makeRight 可以调用的私有构造函数中赋值。
  • 2 left is assigned in the private constructor that only makeLeft and makeRight can call.
  • 3 makeLeft 和 makeRight 将 left 初始化为合适的值。
  • 3 makeLeft and makeRight initialize left to the appropriate value.

当我们要将值取出来时,作为调用者,我们有责任首先检查该值是两种类型中的哪一种。如果我们有一个Either<Triangle, Square>并且想要一个Triangle,我们首先调用isLeft()。如果true返回,我们调用getLeft()并以 结束Triangle,如以下清单所示。

When we want to take the value out, as a caller, it is our responsibility to first check which of the two types the value is. If we have an Either<Triangle, Square> and want a Triangle, we start by calling isLeft(). If true is returned, we call getLeft() and end up with a Triangle, as the following listing shows.

清单 4.11。Triangle或者Square
声明 const triangleType:唯一符号;
三角形类 {                               1
    [三角形类型]: void;
    /* ... */
}

声明 const squareType:唯一符号;
类广场{
    [方型]: void;                       1个
    /* ... */
}

函数槽(三角形:三角形){
    /* ... */
}

让 myTriangle: Either<Triangle,Square>
    = Either.makeLeft(新三角形());        2个

如果 (myTriangle.isLeft())
    插槽(myTriangle.getLeft());               3个
declare const triangleType: unique symbol;
class Triangle {                              1
    [triangleType]: void;
    /* ... */
}

declare const squareType: unique symbol;
class Square {
    [squareType]: void;                       1
    /* ... */
}

function slot(triangle: Triangle) {
    /* ... */
}

let myTriangle: Either<Triangle,Square>
    = Either.makeLeft(new Triangle());        2

if (myTriangle.isLeft())
    slot(myTriangle.getLeft());               3

  • 1 三角形和方形类型
  • 1 Triangle and Square types
  • 2 从这里开始,myTriangle.value 的类型为 Triangle | 正方形; 编译器不再知道我们在那里放置了一个三角形。
  • 2 From here on, myTriangle.value is of type Triangle | Square; the compiler no longer knows that we placed a Triangle there.
  • 3 getLeft() 将值转换回 Triangle。
  • 3 getLeft() casts the value back to a Triangle.

在内部,我们的getLeft()实现执行它需要的任何检查(在这种情况下通过检查 is this.isLeft()true并处理我们想要的无效调用(在这种情况下通过抛出Error)。当所有这些都不碍事时,它将值转换为类型。当我们分配了它,所以现在我们提醒它,如下面的代码所示,因为我们一直在跟踪left.

Internally, our getLeft() implementation performs whatever checks it needs (in this case by checking that this.isLeft() is true) and handles an invalid call however we want (in this case by throwing Error). When all that is out of the way, it casts the value to the type. The type checker forgot which type the value was when we assigned it, so now we remind it, as shown in the following code, as we were keeping track of the type in left.

清单 4.12。isLeft()getLeft()
类 Either<TLeft, TRight> {
    私有只读值:TLeft | 对;
    私有只读左:布尔值;

    /* ... */

    isLeft(): 布尔值 {
        返回 this.left;                        1个
    }

    getLeft(): TLeft {
        如果(!this.isLeft())抛出新的错误();   2个

        返回 <TLeft>this.value;                3个
    }

    /* ... */
}
class Either<TLeft, TRight> {
    private readonly value: TLeft | TRight;
    private readonly left: boolean;

    /* ... */

    isLeft(): boolean {
        return this.left;                        1
    }

    getLeft(): TLeft {
        if (!this.isLeft()) throw new Error();   2

        return <TLeft>this.value;                3
    }

    /* ... */
}

  • 1 客户端可以通过调用isLeft()检查存储的值是否为TLeft类型。
  • 1 Clients can check whether the value stored is of type TLeft by calling isLeft().
  • 2 如果值类型错误,我们可以处理错误。在这种情况下,我们抛出一个错误。另一种方法是返回未定义。
  • 2 In case the value is of the wrong type, we can handle the error. In this case, we throw an Error. An alternative would be to return undefined.
  • 3 该值被转换为 TLeft 类型。
  • 3 The value is cast to the type TLeft.

在这种情况下,我们不需要<unknown>强制转换:类型的值TLeft | TRight可以是 type 的有效值TLeft,因此编译器不会抱怨并且会信任我们进行强制转换。

In this case, we don’t need the <unknown> cast: a value of the type TLeft | TRight could be a valid value of type TLeft, so the compiler won’t complain and will trust us with the cast.

如果使用得当,转换功能非常强大,因为它允许我们细化值的类型。如果我们有一个Triangle | Square, 并且我们知道它是一个Triangle, 我们可以将它转换为一个Triangle, 编译器将允许我们通过一个三角形槽来适应它。

When used correctly, casting is powerful because it allows us to refine the type of a value. If we have a Triangle | Square, and we know that it is a Triangle, we can cast it to a Triangle, which the compiler will allow us to fit through a triangular slot.

事实上,大多数类型检查器会自动执行多个此类转换,而无需我们编写任何代码。

In fact, most type checkers do several such casts automatically without requiring us to write any code.

隐式和显式类型转换

隐式类型转换,也称为强制转换,是由编译器自动执行的类型转换。它不需要编写任何代码。这样的演员表通常是安全的。相比之下,显式类型转换是我们需要用代码指定的类型转换。这种类型转换有效地绕过了类型系统的规则,我们应该小心使用它。

An implicit type cast, also known as coercion, is a type cast that is performed automatically by the compiler. It doesn’t require any code to be written. Such casts are usually safe. By contrast, an explicit type cast is a type cast that we need to specify with code. This type cast effectively bypasses the rules of the type system, and we should use it with care.

4.3.3.普通类型转换

4.3.3. Common type casts

让我们看一下几种常见的类型转换(隐式和显式),看看它们有何用处。

Let’s look at a few common types of casts, both implicit and explicit, and see how they can be useful.

向上转型和向下转型

常见类型转换的一个示例是将从另一种类型继承的类型的对象解释为其父类型。如果我们的基类是Shape,并且我们有一个Triangle,我们总是可以在需要Trianglea 时使用 a Shape,如以下代码所示。

One example of a common type cast is interpreting an object of a type that inherits from another type as its parent type. If our base class is Shape, and we have a Triangle, we can always use a Triangle whenever a Shape is required, as shown in the following code.

清单 4.13。向上广播
类形状{
    /* ... */
}

声明 const triangleType:唯一符号;

三角形类扩展形状 {               1
    [三角形类型]: void;
    /* ... */
}

函数 useShape(形状:形状){            2
    /* ... */
}

让我的三角形:三角形=新三角形();

使用形状(我的三角形);                       3个
class Shape {
    /* ... */
}

declare const triangleType: unique symbol;

class Triangle extends Shape {              1
    [triangleType]: void;
    /* ... */
}

function useShape(shape: Shape) {           2
    /* ... */
}

let myTriangle: Triangle = new Triangle();

useShape(myTriangle);                       3

  • 1 Triangle 类型扩展了 Shape。
  • 1 The Triangle type extends Shape.
  • 2 useShape() 需要一个 Shape 类型的参数。
  • 2 useShape() expects an argument of type Shape.
  • 3 我们可以传一个Triangle给它,它会自动转换成Shape。
  • 3 We can pass a Triangle to it, and it is automatically cast to Shape.

在 的主体内useShape(),编译器将参数视为一个Shape,即使我们传入了一个Triangle。将派生类 ( Triangle) 解释为基类 ( Shape) 称为向上转型。如果我们确定我们的 Shape 实际上是一个三角形,我们可以将它转换回Triangle,但是这个转换需要是显式的。从父类到派生类的转换称为向下转换,如下一个清单所示,大多数强类型语言不会自动执行此操作。

Inside the body of useShape(), the compiler treats the argument as a Shape, even if we passed in a Triangle. Interpreting a derived class (Triangle) as a base class (Shape) is called an upcast. If we know for sure that our Shape is actually a Triangle, we can cast it back to Triangle, but this cast needs to be explicit. Casting from a parent class to a derived class is called a downcast, shown in the next listing, and most strongly typed languages don’t do this automatically.

清单 4.14。沮丧
类形状{
    /* ... */
}

声明 const triangleType:唯一符号;

类三角形扩展形状{
    [三角形类型]: void;
    /* ... */
}

function useShape(shape: Shape, isTriangle: boolean ) {     1
    如果(是三角形){
        let triangle: Triangle = <Triangle>形状;         2个
        /* ... */
    }
    /* ... */
}

让我的三角形:三角形=新三角形();

使用形状(我的三角形,);                               3个
class Shape {
    /* ... */
}

declare const triangleType: unique symbol;

class Triangle extends Shape {
    [triangleType]: void;
    /* ... */
}

function useShape(shape: Shape, isTriangle: boolean) {    1
    if (isTriangle) {
        let triangle: Triangle = <Triangle>shape;         2
        /* ... */
    }
    /* ... */
}

let myTriangle: Triangle = new Triangle();

useShape(myTriangle, true);                               3

  • 1 此版本的函数有一个附加参数,用于跟踪是否传入了三角形。
  • 1 This version of the function has an additional argument that tracks whether a triangle was passed in.
  • 2 如果参数实际上是一个三角形,我们可以通过强制转换取回类型。
  • 2 If the argument is in fact a triangle, we can get the type back with a cast.
  • 3 调用者需要正确设置该标志;否则,会发生运行时错误。
  • 3 The caller needs to set this flag correctly; otherwise, a run-time error occurs.

与向上转型不同,向下转型并不安全。虽然很容易从派生类中分辨出它的父类是什么,但是编译器无法自动确定给定父类,一个值可能是哪个可能的派生类。

Unlike an upcast, a downcast is not safe. Although it’s easy to tell from a derived class what its parent is, the compiler can’t automatically determine, given a parent class, which of the possible derived classes a value might be.

一些编程语言在运行时存储额外的类型信息,并包含一个is运算符,可用于查询对象的类型。当我们创建一个新对象时,它的关联类型存储在旁边,所以即使我们从编译器中向上转换了一些类型信息,在运行时我们也可以检查我们是否有一个特定类型的实例if (shape is Triangle) ...

Some programming languages store additional type information at run time and include an is operator, which can be used to query the type of an object. When we are creating a new object, its associated type is stored alongside, so even if we upcast away some of the type information from the compiler, at run time we can check whether we have an instance of a certain type with if (shape is Triangle) ....

实现这种运行时类型信息的语言和运行时提供了一种更安全的存储和查询类型的方法,因为不存在此信息与对象不同步的风险。这是以为每个对象实例在内存中存储额外数据为代价的。

Languages and run times that implement this kind of run-time type information provide a safer way to store and query for types, as there is no risk that this information will get out of sync with the objects. This comes at the cost of storing additional data in memory for each object instance.

第 7 章中,当我们讨论子类型时,我们将研究更复杂的向上转型并讨论方差。现在,我们将继续讨论扩大和缩小演员表。

In chapter 7, when we discuss subtyping, we will look at more complex upcasts and talk about variance. For now, we’ll move on to talk about widening and narrowing casts.

扩大铸型和缩小铸型

另一种常见的隐式转换是从具有固定位数的整数类型(例如 8 位无符号整数)到另一种表示具有更多位数的值的整数类型(例如 16 位无符号整数)。您可以隐式执行此操作,因为 16 位无符号整数可以表示任何 8 位无符号整数值等等。这种类型的铸件称为加宽铸件

Another common implicit cast is from an integer type with a fixed number of bits—say, an 8-bit unsigned integer—to another integer type that represents values with more bits—say, a 16-bit unsigned integer. You can do this implicitly because a 16-bit unsigned integer can represent any 8-bit unsigned integer value and more. This type of cast is called a widening cast.

另一方面,将有符号整数转换为无符号整数是危险的,因为负数不能用无符号整数表示。类似地,将位数较多的整数转换为位数较少的整数,例如将 16 位无符号整数转换为 8 位无符号整数,仅适用于较小类型可以表示的值。

On the other hand, casting a signed integer to an unsigned integer is dangerous, as a negative number can’t be represented by an unsigned integer. Similarly, casting an integer with more bits to an integer with fewer bits, such as a 16-bit unsigned integer to an 8-bit unsigned integer, would work only for values that the smaller type can represent.

这种类型的转换称为缩小转换。某些编译器会强制您在执行缩小转换时明确表示,因为这很危险。明确有帮助,因为它清楚地表明您不是无意中这样做的。其他编译器允许缩小转换但发出警告。值不适合新值时的运行时行为type 类似于我们在第 2 章中讨论的整数溢出:根据语言的不同,我们会得到错误或值被截断以适合新类型(图 4.5)。

This type of cast is called a narrowing cast. Some compilers force you to be explicit when performing a narrowing cast because it’s dangerous. Being explicit helps, in that it makes it clear you didn’t do it unintentionally. Other compilers allow narrowing casts but issue a warning. Run-time behavior when the value doesn’t fit the new type is similar to the integer overflow that we discussed in chapter 2: depending on the language, we get an error or the value gets chopped so that it fits in the new type (figure 4.5).

图 4.5。扩大和缩小铸件的示例。加宽转换是安全的:灰色方块代表我们得到的额外位,因此不会丢失任何信息。另一方面,缩小转换是危险的:黑色方块代表不再适合新类型的位。

不要轻易使用强制转换,因为它们绕过了类型检查器,有效地消除了类型检查给我们带来的所有好处。但是,它们是有用的工具,尤其是当我们拥有比编译器更多的信息并希望将该信息返回给编译器时。在我们告诉编译器我们所知道的之后,它可以在进一步分析中使用该信息。回到这个Triangle | Square例子,在我们告诉编译器我们的值是 a 之后,后面Triangle就没有任何值了。Square这种技术类似于 4.2 节中讨论的技术,我们在其中研究了强制约束,但在这里,我们不执行运行时检查,而是简单地告诉编译器信任我们。

Casts are not to be used lightly, as they bypass the type checker, effectively eliminating all the goodness that type checking brings us. They are useful tools, though, especially when we have more information than the compiler does and want to push that information back to the compiler. After we tell the compiler what we know, it can use that information in further analysis. Going back to the Triangle | Square example, after we tell the compiler our value is a Triangle, there can be no Square value farther on. This technique is similar to the one discussed in section 4.2, in which we looked at enforcing constraints, but here, instead of performing a run-time check, we simply tell the compiler to trust us.

在下一节中,我们将研究一些其他情况,在这些情况下,让编译器“忘记”键入信息很有用。

In the next section, we’ll look at a few other situations in which it’s useful to make the compiler “forget” typing information.

4.3.4.练习

4.3.4. Exercises

1个

以下哪些演员表被认为是安全的?

  1. 向上转换
  2. 沮丧
  3. 向上转型和向下转型
  4. 两者都不

1

Which of the following casts are considered to be safe?

  1. Upcasts
  2. Downcasts
  3. Upcasts and downcasts
  4. Neither

2个

以下哪些转换被认为是不安全的?

  1. 加宽铸件
  2. 缩小铸型
  3. 扩大和缩小铸件
  4. 两者都不

2

Which of the following casts are considered to be unsafe?

  1. Widening casts
  2. Narrowing casts
  3. Widening and narrowing casts
  4. Neither

4.4. 隐藏和恢复类型信息

4.4. Hiding and restoring type information

隐藏类型信息的一个示例是希望拥有一个可以包含不同类型值组合的集合。如果集合只包含一种类型的值,例如一袋猫,这很容易,因为我们知道每当我们从袋子里拿出一些东西时,它都会是一只猫。如果我们也想把杂货放在袋子里,当我们拿出东西时,我们最终可能会得到一只猫或一件杂货(图 4.6)。

One example of hiding type information is wanting to have a collection that can contain a combination of values of different types. If the collection contains values of just one type, such as a bag of cats, it’s easy, because we know that whenever we pull some thing out from the bag, it’s going to be a cat. If we want to put groceries in the bag too, when we pull something out, we might end up with either a cat or a grocery item (figure 4.6).

图 4.6。如果我们有一个只装猫的袋子,我们可以打赌,无论我们从袋子里拿出什么东西,都会是一只猫。如果袋子里也能装杂货,我们就不能再保证能拿出什么东西了。

具有相同类型物品的集合,例如我们的猫袋,也称为同质集合。因为所有项目都有相同的类型,所以我们不需要隐藏它们的类型信息。不同类型项目的集合也称为异构集合。在这种情况下,我们需要隐藏一些类型信息来声明这样一个集合。

A collection with items of the same type, like our bag of cats, is also called a homogenous collection. Because all items have the same type, we don’t need to hide their type information. A collection of items of different types is also known as a heterogenous collection. In this case, we need to hide some of the typing information to declare such a collection.

4.4.1.异构集合

4.4.1. Heterogenous collections

文档可以包含文本、图片或表格。当我们处理文档时,我们希望将其所有组成部分放在一起,因此我们将它们存储在某个集合中。但是该集合的元素类型是什么?有几种方法可以实现这一点,所有这些方法都涉及隐藏一些类型信息。

A document can contain text, pictures, or tables. When we work with the document, we want to keep all its constituent parts together, so we will store them in some collection. But what is the type of the elements of that collection? There are several ways to implement this, all of which involve hiding some type information.

基本类型或接口

我们可以创建一个类层次结构,并说文档中的所有项目都必须是某个层次结构的一部分。如果一切都是 a DocumentItem,我们就可以存储值的集合DocumentItem,即使当我们向集合中添加项时,我们添加了ParagraphPicture和 等类型Table。类似地,我们可以声明一个IDocumentItem接口并说该数组只包含实现该接口的类型,如以下清单所示。

We can create a class hierarchy and say that all items in the documents must be part of some hierarchy. If everything is a DocumentItem, we can store a collection of DocumentItem values even if, when we add items to the collection, we add types such as Paragraph, Picture, and Table. Similarly, we can declare an IDocumentItem interface and say that the array contains only types that implement this interface, as shown in the following listing.

清单 4.15。实现类型的集合IDocumentItem
接口 IDocumentItem {                      1
    /* ... */
}

类段落实现 IDocumentItem {     2
    /* ... */
}

类图片实现 IDocumentItem {       2
    /* ... */
}

类表实现 IDocumentItem {         2
    /* ... */
}

类我的文档{
    项目:IDocumentItem[];                   3个

    /* ... */
}
interface IDocumentItem {                     1
    /* ... */
}

class Paragraph implements IDocumentItem {    2
    /* ... */
}

class Picture implements IDocumentItem {      2
    /* ... */
}

class Table implements IDocumentItem {        2
    /* ... */
}

class MyDocument {
    items: IDocumentItem[];                   3

    /* ... */
}

  • 1 IDocumentItem 是文档元素的通用接口。
  • 1 IDocumentItem is the common interface for document elements.
  • 2 Paragraph、Picture 和Table 都实现了IDocumentItem。
  • 2 Paragraph, Picture, and Table all implement IDocumentItem.
  • 3 我们将文档项存储为 IDocumentItem 对象的数组。
  • 3 We store document items as an array of IDocumentItem objects.

我们隐藏了一些类型信息,因此我们不再知道集合中的特定项目是 a Paragraph、 aPicture还是 a Table,但我们知道它实现了DocumentItemorIDocumentItem协定。如果我们只需要该契约指定的行为,我们可以按原样使用集合的元素。如果我们需要一个确切的类型,例如我们想要传递给图像增强插件的图片,我们必须将 or 向下DocumentItem转换IDocumentItemPicture.

We’ve hidden some of the typing information, so we no longer know whether a particular item in the collection is a Paragraph, a Picture, or a Table, but we know that it implements the DocumentItem or IDocumentItem contract. If we need only behavior specified by that contract, we can work with the elements of the collection as is. If we need an exact type, such as a picture that we want to pass to an image-enhancing add-on, we have to downcast the DocumentItem or IDocumentItem back to a Picture.

求和类型或变体

如果我们预先知道我们正在处理的所有类型,我们可以使用求和类型,如清单 4.16所示。我们可以将我们的文档定义为一个数组Paragraph | Picture | Table(在这种情况下,我们必须通过其他方式跟踪集合中的每个项目是什么)或定义为一个类型Variant<Paragraph, Picture, Table>(它在内部跟踪它存储的类型)。

If we know up front all the types we are dealing with, we can use a sum type, as shown in listing 4.16. We can define our document as an array of Paragraph | Picture | Table (in which case we must track what each item in the collection is by some other means) or as a type such as Variant<Paragraph, Picture, Table> (which keeps track internally of the type it stores).

清单 4.16。作为求和类型的类型集合
类段落 {                               1
    /* ... */
}

类图片 {                                 1
    /* ... */
}

类表 {                                   1
    /* ... */
}

类我的文档{
    项目:(段落|图片|表格)[]2个

    /* ... */
}
class Paragraph {                              1
    /* ... */
}

class Picture {                                1
    /* ... */
}

class Table {                                  1
    /* ... */
}

class MyDocument {
    items: (Paragraph | Picture | Table)[];    2

    /* ... */
}

  • 1 段落、图片和表格不再实现接口。
  • 1 Paragraph, Picture, and Table no longer implement an interface.
  • 2 文档项集合现在是一个对象数组,可以是任何一种类型。
  • 2 The document item collection is now an array of objects that can be either of the types.

和选项Paragraph | Picture | TableVariant<Paragraph, Picture, Table>允许我们存储一组不需要有任何共同点(没有共同的基类型或实现的接口)的项目。优点是我们不会对集合中的类型强加任何东西。缺点是如果不将列表中的项目转换回它们的实际类型,或者在这种情况下,Variant调用visit()并必须为集合中的每个可能类型提供函数,我们对列表中的项目无能为力。

Both Paragraph | Picture | Table and Variant<Paragraph, Picture, Table> options allow us to store a set of items that don’t need to have anything in common (no common base type or implemented interface). The advantage is that we don’t impose anything on the types in the collection. The disadvantage is that there is not much we can do with the items in the list without casting them back down to their actual types or, in the Variant case, calling visit()and having to provide functions for each of the possible types in the collection.

提醒一下,因为像这样的类型Variant在内部跟踪它实际存储的类型,就像它一样Either,它知道从传递给的一组函数中选择哪个函数visit()

As a reminder, because a type like Variant keeps track internally of which type it actually stores, just as Either does, it knows which function to pick from a set of functions passed to visit().

未知类型

在极端情况下,我们可以说我们有一个可以包含任何东西的集合。如清单 4.17所示,TypeScript 提供了unknown表示该类型集合的类型。大多数面向对象的编程语言都有一个共同的基类型,它是所有其他类型的父类,通常称为Object. 我们将在第 7 章讨论子类型时 深入讨论这个话题。

At an extreme, we can say we have a collection that can contain anything. As shown in listing 4.17, TypeScript provides the type unknown to represent that type of collection. Most object-oriented programming languages have a common base type that is the parent of all other types, usually called Object. We’ll cover this topic in depth in chapter 7 when we discuss subtyping.

清单 4.17。unknown类型 的集合
类我的文档{
    项目:未知[];      1个
    /* ... */
}
class MyDocument {
    items: unknown[];      1
    /* ... */
}

  • 1 数组的元素可以是任何东西。
  • 1 The elements of the array can be anything.

这种技术使我们能够拥有包含任何内容的文档。类型不需要共享契约,我们甚至不需要事先知道类型的作用。另一方面,我们对这个系列的元素能做的就更少了。我们几乎总是必须将它们转换为其他类型,因此我们必须以另一种方式跟踪它们的原始类型。

This technique allows us to have a document containing anything. Types don’t need to have a shared contract, and we don’t even need to know beforehand what the types do. On the other hand, there’s even less we can do with the elements of this collection. We’ll almost always have to cast them to other types, so we have to keep track of their original types in another way.

表 4.1总结了不同的方法和权衡。

Table 4.1 summarizes the different approaches and trade-offs.

表 4.1。异构列表实现的优缺点
 

优点

Pros

缺点

Cons

等级制度 无需转换即可轻松使用基类型的任何属性或方法 集合中的类型必须通过基类型或实现的接口相关联
和式 不要求类型相关 如果我们没有 Variant 的 visit(),则需要转换回实际类型以使用项目
未知类型 可以存储任何东西 需要跟踪实际类型并转换回它们以使用项目

所有这些例子都有利有弊,这取决于我们希望我们的集合在可以存储什么方面有多灵活,以及我们希望多久将项目恢复到它们的原始类型。也就是说,当我们将项目放入集合中时,所有示例都隐藏了一些类型信息。隐藏和恢复类型信息的另一个例子是序列化。

All these examples have pros and cons, depending on how flexible we want our collection to be in terms of what can be stored there and how often we expect to have to restore the items to their original types. That being said, all the examples hide some amount of type information when we put items in the collection. Another example of hiding and restoring type information is serialization.

4.4.2.连载

4.4.2. Serialization

当我们将信息写入文件并希望将其加载回来并在我们的程序中使用它时,或者当我们连接到互联网服务并发送和检索一些数据时,该数据以位序列的形式传输。序列化是获取特定类型的值并将其编码为位序列的过程。相反的操作,反序列化,涉及获取一个位序列并将其解码为我们可以使用的数据结构(图 4.7)。

When we write information to a file and want to load it back and use it in our program, or when we connect to an internet service and send and retrieve some data, that data travels as a sequence of bits. Serialization is the process of taking a value of a certain type and encoding it as a sequence of bits. The opposite operation, deserialization, involves taking a sequence of bits and decoding it into a data structure we can work with (figure 4.7).

图 4.7。具有两扇门和前轮驱动的紧凑型汽车序列化为 JSON,然后反序列化回汽车

确切的编码取决于我们使用的协议。它可以是 JSON、XML 或任何其他可用协议。从类型的角度来看,重要的部分是在序列化之后,我们最终得到的值应该等同于我们开始使用的类型化值,但是类型系统无法获得所有类型化信息。实际上,我们最终得到一个字符串或一个字节数组。该JSON.stringify()方法接受一个对象并返回一个 JSON 表示形式该对象作为字符串。如果我们将 a 字符串化Cat,如下一个清单所示,我们可以将结果写入磁盘、网络甚至屏幕,但我们无法将其写入meow()

The exact encoding depends on the protocol we use. It can be JSON, XML, or any other of the multitude of available protocols. From a type perspective, the important part is that after serialization, we end up with a value that should be equivalent to the typed value we started with, but all typing information becomes unavailable to the type system. Effectively, we end up with a string or an array of bytes. The JSON.stringify() method takes an object and returns a JSON representation of that object as a string. If we stringify a Cat, as the next listing shows, we can write the result to disk, to the network, or even to the screen, but we cannot get it to meow().

清单 4.18。序列化一个cat
类猫{
    喵(){                                               1
        /* ... */
    }
}

让 serializedCat: string = JSON.stringify(new Cat());    2个

// serializeCat.meow();                                   3个
class Cat {
    meow() {                                              1
        /* ... */
    }
}

let serializedCat: string = JSON.stringify(new Cat());    2

// serializeCat.meow();                                   3

  • 1 具有 meow() 方法的 Cat 类型。
  • 1 A Cat type that has a meow() method.
  • 2 我们使用 JSON.stringify() 将 Cat 对象序列化为 JSON 字符串。
  • 2 We serialize a Cat object as a JSON string by using JSON.stringify().
  • 3 很明显,我们不能使用meow()这样的方法,因为serializedCat是一个字符串。
  • 3 Obviously, we can’t use a method like meow() because serializedCat is a string.

我们仍然知道值是什么,但类型检查器不再知道了。相反的操作涉及获取一个序列化对象并将其转回类型化值。在这种情况下,我们可以使用该JSON.parse()方法,它接受一个字符串并返回一个 JavaScript 对象。因为这种技术适用于任何字符串,所以调用它的结果是 type any

We still know what the value is, but the type checker no longer does. The opposite operation involves taking a serialized object and turning it back into a typed value. In this case, we can use the JSON.parse() method, which takes a string and returns a JavaScript object. Because this technique works for any string, the result of calling it is of type any.

任何类型

TypeScript 提供了一种any类型。当键入信息不可用时,此类型用于与 JavaScript 的互操作性。any是一种危险的类型,因为编译器不对此类型的实例进行类型检查,它可以自由地与任何其他类型相互转换。开发人员应确保不会发生误解。

TypeScript provides an any type. This type is used for interoperability with JavaScript when typing information is unavailable. any is a dangerous type because the compiler does no type checking on instances of this type, which can be freely converted to and from any other type. It’s up to the developer to ensure that no misinterpretations happen.

如果我们知道我们有一个 serialized Cat,我们可以将它分配给一个新Cat对象Object.assign(),如下面的清单所示,然后将它转换回它的类型,因为Object.assign()返回一个 type 的值any

If we know that we have a serialized Cat, we can assign it to a new Cat object by using Object.assign() as shown in the following listing, and then cast it back to its type, as Object.assign() returns a value of type any.

清单 4.19。反序列化Cat
类猫{
    喵() {
        /* ... */
    }
}

让 serializedCat: string = JSON.stringify(new Cat());

让反序列化猫:猫=
    <Cat>Object.assign(new Cat(), JSON.parse(serializedCat));    1个

deserializedCat.meow();                                          2个
class Cat {
    meow() {
        /* ... */
    }
}

let serializedCat: string = JSON.stringify(new Cat());

let deserializedCat: Cat =
    <Cat>Object.assign(new Cat(), JSON.parse(serializedCat));    1

deserializedCat.meow();                                          2

  • 1 我们使用 JSON.parse() 反序列化对象,将其分配给一个新的 Cat 实例,并将其转换为 Cat 类型。
  • 1 We deserialize the object by using JSON.parse(), assign it to a new Cat instance, and cast it to the Cat type.
  • 2 我们可以在对象上调用 meow(),因为它是 Cat 类型并且有一个 meow() 方法。
  • 2 We can call meow() on the object, as it is of type Cat and has a meow() method.

在某些情况下,我们可以获取并反序列化任意数量的可能类型,在这种情况下,在序列化对象中也对一些类型信息进行编码可能是个好主意。我们可以定义一个协议,其中每个对象都以代表其类型的字符为前缀。然后我们可以对 a 进行编码Cat并在结果字符串前加上"c"for Cat。如果我们得到一个序列化对象,我们检查第一个字符。如果是"c",我们可以安全地恢复我们的Cat. 如果是"d", 对于Dog,我们知道不要反序列化 a Cat,如以下清单所示。

In some cases, we can get and deserialize any number of possible types, in which case it might be a good idea to encode some of the typing information in the serialized object too. We can define a protocol in which each object is prefixed with a character that represents its type. Then we can encode a Cat and prefix the resulting string with "c" for Cat. If we get a serialized object, we check the first character. If it’s "c", we can safely restore our Cat. If it’s "d", for Dog, we know not to deserialize a Cat, as shown in the following listing.

清单 4.20。序列化和跟踪类型
类猫{
    喵() { /* ... */ }
}

类狗{
    树皮() { /* ... */ }
}

函数序列化猫(猫:猫):字符串{
    返回 "c" + JSON.stringify(cat);                                  1个
}

函数序列化狗(狗:狗):字符串{
    返回 "d" + JSON.stringify(dog);                                  2个
}

函数 tryDeserializeCat(from: string): Cat | 未定义 {             3 
    if (from[0] != "c") 返回未定义;                              4个

    返回 <Cat>Object.assign(new Cat(), JSON.parse(from.substr(1)));  5 
}
class Cat {
    meow() { /* ... */ }
}

class Dog {
    bark() { /* ... */ }
}

function serializeCat(cat: Cat): string {
    return "c" + JSON.stringify(cat);                                  1
}

function serializeDog(dog: Dog): string {
    return "d" + JSON.stringify(dog);                                  2
}

function tryDeserializeCat(from: string): Cat | undefined {            3
    if (from[0] != "c") return undefined;                              4

    return <Cat>Object.assign(new Cat(), JSON.parse(from.substr(1)));  5
}

  • 1 我们通过在 JSON 表示形式前加上“c”来序列化 Cat 对象。
  • 1 We serialize a Cat object by prefixing a “c” to the JSON representation.
  • 2 我们通过在 JSON 表示形式前加上“d”来序列化 Dog 对象。
  • 2 We serialize a Dog object by prefixing a “d” to the JSON representation.
  • 3 给定序列化的 Cat 或 Dog,我们可以尝试反序列化 Cat。
  • 3 Given a serialized Cat or Dog, we can attempt to deserialize a Cat.
  • 4 如果第一个字符不是“c”,返回undefined 因为我们不能反序列化一个Cat。
  • 4 If the first character is not “c”, return undefined because we can’t deserialize a Cat.
  • 5 否则,JSON.parse() 字符串的其余部分并将其分配给 Cat 对象。
  • 5 Otherwise, JSON.parse() the rest of the string and assign it to a Cat object.

如果我们序列化一个Cat对象并调用tryDeserializeCat()它的序列化表示,我们会​​得到一个Cat对象。另一方面,如果我们序列化一个Dog对象并调用tryDeserializeCat(),我们会返回未定义的。然后我们可以检查我们是否得到了 anundefined并查看我们是否有Cat,如下一个清单所示。

If we serialize a Cat object and call tryDeserializeCat() on its serialized representation, we get back a Cat object. If, on the other hand, we serialize a Dog object and call tryDeserializeCat(), we get back undefined. Then we can check to see whether we got an undefined and see whether we have a Cat, as shown in the next listing.

清单 4.21。使用跟踪类型反序列化
让 catString: string = serializeCat(new Cat());                 1
让 dogString: string = serializeDog(new Dog());                 1个

让 maybeCat: 猫 | undefined = tryDeserializeCat(catString);    2个

if (maybeCat != undefined) {                                      3 
    let cat: Cat = <Cat>maybeCat;                                4
    猫.喵();                                                  4个
}

maybeCat = tryDeserializeCat(dogString);                         5个
let catString: string = serializeCat(new Cat());                 1
let dogString: string = serializeDog(new Dog());                 1

let maybeCat: Cat | undefined = tryDeserializeCat(catString);    2

if (maybeCat != undefined) {                                     3
    let cat: Cat = <Cat>maybeCat;                                4
    cat.meow();                                                  4
}

maybeCat = tryDeserializeCat(dogString);                         5

  • 1 我们将 Cat 和 Dog 序列化为字符串。
  • 1 We serialize a Cat and a Dog to strings.
  • 2 调用 tryDeserializeCat 给我们一个 Cat 或 undefined。
  • 2 Calling tryDeserializeCat gives us either a Cat or undefined.
  • 3 我们可以检查我们是否得到了一只猫。
  • 3 We can check whether we got a Cat.
  • 4 如果我们这样做了,我们可以转换为 Cat 并获得一个我们可以调用 meow() 的对象。
  • 4 If we did, we can cast to Cat and get an object we can call meow() on.
  • 5 尝试从序列化的 Dog 对象反序列化 Cat 对象将返回 undefined。
  • 5 Attempting to deserialize a Cat object from a serialized Dog object will give us undefined.

我们之所以可以maybeCat与进行比较,即使我们之前undefined无法Triangle与进行比较,是因为它是 TypeScript 中的一种特殊单位类型。该类型有一个可能的值,即. 在没有这种类型的情况下,我们总是可以使用像. 我们在第 3 章中将类型描述为包含类型值或无值的类型。 TLeftundefinedundefinedundefinedOptional<Cat>Optional<T>T

The reason why we can compare maybeCat with undefined, even though we couldn’t compare Triangle with TLeft previously, is that undefined is a special unit type in TypeScript. The undefined type has a single possible value, which is undefined. In the absence of this type, we can always use a type like Optional<Cat>. We described Optional<T> in chapter 3 as a type that contains a value of type T or nothing.

正如我们在本章中所看到的,类型为我们的代码提供了全新的安全级别。我们可以捕获类型声明中隐含的假设,并通过避免原始的痴迷并让类型检查器确保我们不会误解值来使它们显式。我们可以进一步限制某个类型的允许值,并确保在创建实例时满足约束,这样我们就有了一个保证,当我们有一个给定类型的实例时,它永远有效。

As we’ve seen throughout this chapter, types enable whole new levels of safety for our code. We can capture what would’ve been implicit assumptions in type declaration and make them explicit by avoiding primitive obsession and letting the type checker make sure that we don’t misinterpret values. We can further restrict the allowed values of a certain type and ensure that constraints are met during instance creation, so that we have a guarantee that when we have an instance of a given type, it will always be valid.

另一方面,我们希望在某些情况下更加灵活,以相同的方式处理多种类型。在这种情况下,我们可以隐藏一些类型信息并扩展变量可以取的可能值。在大多数情况下,我们仍然希望跟踪值的原始类型,以便稍后恢复它。我们通过将类型存储在其他地方(例如另一个变量)来在类型系统之外执行此操作。一旦我们不再需要额外的灵活性并希望再次依赖类型检查器,我们就可以通过使用类型转换来恢复类型。

On the other hand, we want to be more flexible in some situations and handle multiple types in the same way. In such situations, we can hide some of the type information and expand the possible values that a variable can take. In most cases, we would still like to keep track of the original type of the value so we can restore it later. We do that outside the type system by storing the type somewhere else, such as in another variable. As soon as we no longer need the extra flexibility and want to rely on the type checker again, we can restore the type by using a type cast.

4.4.3.练习

4.4.3. Exercises

1个

如果我们想为它分配任何可能的值,我们应该使用哪种类型?

  1. any
  2. unknown
  3. any | unknown
  4. 要么any_unknown

1

Which type should we use if we want to assign any possible value to it?

  1. any
  2. unknown
  3. any | unknown
  4. Either any or unknown

2个

表示数字和字符串数组的最佳方式是什么?

  1. (number | string)[]
  2. number[] | string[]
  3. unknown[]
  4. any[]

2

What is the best way to represent an array of numbers and strings?

  1. (number | string)[]
  2. number[] | string[]
  3. unknown[]
  4. any[]

概括

Summary

  • 当我们将值声明为基本类型并对它们的含义做出隐式假设时,就会出现原始的痴迷反模式。
  • The primitive obsession antipattern shows up when we declare values as basic types and make implicit assumptions about their meaning.
  • 使用原始痴迷的替代方法是定义明确捕获值含义并防止误解的类型。
  • The alternative to using primitive obsession is defining types that explicitly capture the meaning of the values and prevent misinterpretations.
  • 如果我们有额外的约束想要施加但不能在编译时施加,我们可以在构造函数或工厂中强制执行它们,这样当我们有一个类型的对象时,我们就可以保证它是有效的。
  • If we have additional constraints that we want to impose but can’t at compile time, we can enforce them in constructors or factories, so that when we have an object of the type, we are guaranteed that it is valid.
  • 有时,我们比类型检查器知道的更多,因为我们可以将类型信息作为数据存储在类型系统本身之外。
  • Sometimes, we know more than the type checker does, as we can store typing information outside the type system itself as data.
  • 我们可以使用此信息来执行安全类型转换,为类型检查器添加更多信息。
  • We can use this information to perform safe type casts, adding more information for the type checker.
  • 我们可能希望以相同的方式处理不同的类型,也许将不同类型的值存储在单个集合中或将它们序列化。
  • We may want to treat different types the same way, perhaps to store values of different types in a single collection or serialize them.
  • 我们可以通过转换为包含我们的类型、我们的类型继承自的类型、求和类型或可以存储任何其他类型值的类型来隐藏类型信息。
  • We can hide type information by casting to a type that includes our type, a type our type inherits from, a sum type, or a type that can store values of any other type.

到目前为止,我们已经了解了基本类型、组合它们的方法以及我们可以利用类型系统来提高代码安全性的其他方法。在第 5 章中,我们将看到一些截然不同的东西:当我们可以将类型分配给函数并像对待代码中的任何其他值一样对待函数时,将会有哪些新的可能性向我们敞开?

So far we’ve looked at basic types, ways to compose them, and other ways in which we can leverage the type systems to increase the safety of our code. In chapter 5, we’ll look at something radically different: What new possibilities will be open to us when we can assign types to functions and treat functions like any other values in our code?

习题答案

Answers to exercises

避免原始痴迷以防止误解

Avoiding primitive obsession to prevent misinterpretation

1个

c—指定测量单位是一种更安全的方法。

1

c—Specifying the measurement unit is a safer approach.

 

 

强制约束

Enforcing constraints

1个

这是一个可能的解决方案:

声明 const percentageType:唯一符号;

类百分比{
    只读值:数字;
    [百分比类型]:无效;

    私有构造函数(值:数字){
        this.value = 值;
    }

   静态 makePercentage(值:数字):百分比 {
        如果(值 < 0)值 = 0;
        如果(值 > 100)值 = 100;

        返回新的百分比(值);
    }
}

1

Here is a possible solution:

declare const percentageType: unique symbol;

class Percentage {
    readonly value: number;
    [percentageType]: void;

    private constructor(value: number) {
        this.value = value;
    }

   static makePercentage(value: number): Percentage {
        if (value < 0) value = 0;
        if (value > 100) value = 100;

        return new Percentage(value);
    }
}

 

 

添加类型信息

Adding type information

1个

a—向上转型是安全的(将子类型转换为父类型)。

1

a—Upcasts are safe (casting child to parent type).

2个

b—缩小转换是不安全的(可能会丢失信息)。

2

b—Narrowing casts are unsafe (might lose information).

 

 

隐藏和恢复类型信息

Hiding and restoring type information

1个

b—unknown是比 更安全的选择any

1

b—unknown is a safer option than any.

2个

a—unknownany删除过多的类型信息。

2

a—unknown and any remove too much type information.

 

 

第 5 章。函数类型

Chapter 5. Function types

本章涵盖

This chapter covers

  • 使用函数类型简化策略模式
  • Simplifying the strategy pattern with function types
  • switch不使用语句 实现状态机
  • Implementing a state machine without switch statements
  • 将惰性值实现为 lambda
  • Implementing lazy values as lambdas
  • 使用基本的数据处理算法mapfilterreduce来减少代码重复
  • Using the fundamental data processing algorithms map, filter, and reduce to reduce code duplication

我们介绍了基本类型和从它们构建的类型。我们还研究了如何声明新类型以提高程序的安全性并对它们的值实施各种约束。这是关于代数数据类型或将类型组合为求和类型和乘积类型的能力的极限。

We covered basic types and types built up from them. We also looked at how we can declare new types to increase the safety of our programs and enforce various constraints on their values. This is about as far as we can get with algebraic data types or the ability to combine types as sum types and product types.

我们将要介绍的类型系统的下一个特性,它打开了一个全新的表达世界,是类型函数的能力。如果我们可以命名函数类型并在我们使用其他类型的值的相同位置使用函数——如变量、参数和函数返回——我们可以简化几种常用结构的实现,并将常用算法抽象为库函数。

The next feature of type systems we are going to cover, which unlocks a whole new world of expressiveness, is the ability to type functions. If we can name function types and use functions in the same places we use values of other types—as variables, arguments, and function returns—we can simplify the implementation of several common constructs and abstract common algorithms to library functions.

在本章中,我们将研究如何简化策略设计模式的实施。(我们还将快速回顾一下该模式,以防您忘记它。)然后我们将讨论状态机以及如何使用函数属性更简洁地实现它们。我们将介绍惰性值,或者我们如何推迟昂贵的计算以希望我们不需要它。最后,我们将深入探讨基本的map()reduce()filter()算法。

In this chapter, we’ll look at how we can simplify the implementation of the strategy design pattern. (We’ll also have a quick refresher on the pattern, in case you forgot it.) Then we’ll talk about state machines and how they can be implemented more succinctly with function properties. We’ll cover lazy values, or how we can defer expensive computation in the hope that we won’t need it. Finally, we’ll deep dive into the fundamental map(), reduce(), and filter() algorithms.

所有这些应用程序都由功能类型启用,这是继基本类型及其组合之后类型系统发展的下一步。因为现在大多数编程语言都支持这些类型,所以我们将重新审视一些古老的、经过实践检验的概念。

All these applications are enabled by function types, the next step in the evolution of type systems after basic types and their combinations. Because most programming languages nowadays support these types, we’ll get a fresh look at some old, tried, and tested concepts.

5.1. 一个简单的策略模式

5.1. A simple strategy pattern

最常用的设计模式之一是策略模式。策略设计模式是一种行为软件设计模式,可以在运行时从一系列算法中选择一种算法。它将算法与使用它们的组件解耦,从而提高了整个系统的灵活性。该模式通常如图 5.1所示。

One of the most commonly used design patterns is the strategy pattern. The strategy design pattern is a behavioral software design pattern that enables selecting an algorithm at run time from a family of algorithms. It decouples the algorithms from the components using them, which improves the flexibility of the overall system. The pattern is usually presented as in figure 5.1.

图 5.1。IStrategy策略模式由接口ConcreteStrategy1和实现组成ConcreteStrategy2,以及Context通过接口使用算法的模式IStrategy

让我们看一个具体的例子。假设我们有一家洗车店,提供两种服务:标准洗车和高级洗车(额外支付 3 美元,提供额外抛光服务)。

Let’s look at a concrete example. Suppose that we have a car wash with two types of services: a Standard wash and a Premium wash (which, for an extra $3, provides additional polish).

我们可以将这个例子实现为一个策略,其中我们的IWashingStrategy接口提供了一个wash()方法。然后我们提供这个的两个实现界面:一个StandardWash和一个PremiumWash。Our是根据客户支付的服务将 an 应用于汽车的 CarWash上下文。IWashingStrategy.wash()

We can implement this example as a strategy, in which our IWashingStrategy interface provides a wash() method. Then we provide two implementations of this interface: a StandardWash and a PremiumWash. Our CarWash is the context that applies an IWashingStrategy.wash() to a car depending on which service the customer paid for.

清单 5.1。洗车攻略
类车{
    /* 代表一辆车 */                          1
}

接口 IWashingStrategy {                        2
    洗(汽车:汽车):无效;
}

StandardWash 类实现 IWashingStrategy {    3
    洗(车:车):无效{
        /* 执行标准清洗 */
    }
}

PremiumWash 类实现 IWashingStrategy {     3
    洗(车:车):无效{
        /* 执行高级清洗 */
    }
}

类洗车 {
    服务(汽车:汽车,保费:布尔值){
        让洗涤策略:IWashingStrategy;

        如果(高级){                              4
            washingStrategy = new PremiumWash();
        } 别的 {
            washingStrategy = new StandardWash();
        }

        washingStrategy.wash(汽车);                 4个
    }
}
class Car {
    /* Represents a car */                         1
}

interface IWashingStrategy {                       2
    wash(car: Car): void;
}

class StandardWash implements IWashingStrategy {   3
    wash(car: Car): void {
        /* Perform standard wash */
    }
}

class PremiumWash implements IWashingStrategy {    3
    wash(car: Car): void {
        /* Perform premium wash */
    }
}

class CarWash {
    service(car: Car, premium: boolean) {
        let washingStrategy: IWashingStrategy;

        if (premium) {                             4
            washingStrategy = new PremiumWash();
        } else {
            washingStrategy = new StandardWash();
        }

        washingStrategy.wash(car);                 4
    }
}

  • 1 Car 类表示要清洗的汽车。
  • 1 The Car class represents a car to be washed.
  • 2 IWashingStrategy 是我们策略模式的接口,声明了一个 wash() 方法。
  • 2 IWashingStrategy is the interface of our strategy pattern declaring a wash() method.
  • 3 StandardWash 和 PremiumWash 是该策略的具体实施。
  • 3 StandardWash and PremiumWash are concrete implementations of the strategy.
  • 4 根据标志,我们选择要使用的算法,然后用它 wash() 汽车实例。
  • 4 Depending on a flag, we select the algorithm to use and then wash() the car instance with it.

这段代码有效,但不必要地冗长。我们引入了一个接口和两个实现类型,每个类型都提供一个wash()方法。这些类型并不重要;我们代码中有价值的部分是清洗逻辑。这段代码只是一个函数,所以如果我们从接口和类转移到一个函数类型和两个具体实现,我们可以大大简化我们的代码。

This code works, but it is needlessly verbose. We’ve introduced an interface and two implementing types, each providing a single wash() method. These types are not really important; the valuable part of our code is the washing logic. This code is just a function, so we can simplify our code a lot if we move from interfaces and classes to a function type and the two concrete implementations.

5.1.1.功能策略

5.1.1. A functional strategy

我们可以定义WashingStrategy为一个类型,表示一个接收 aCar作为参数并返回 的函数void。然后我们可以将两种类型的清洗实现为两个函数——standardWash()premimumWash()——都接受 aCar并返回void。他们CarWash可以选择其中之一应用于给定的汽车。

We can define WashingStrategy to be a type representing a function that receives a Car as an argument and returns void. Then we can implement the two types of washes as two functions—standardWash() and premimumWash()—both taking a Car and returning void. The CarWash can select one of them to apply to a given car.

清单 5.2。重新审视洗车策略
类车{
    /* 代表一辆车 */
}

类型 WashingStrategy = (car: Car) => void;       1个

函数 standardWash(汽车:汽车):void {           2
    /* 执行标准清洗 */
}

函数 premiumWash(汽车:汽车):void {            2
    /* 执行高级清洗 */
}

类洗车 {
    服务(汽车:汽车,保费:布尔值){
        让洗涤策略:洗涤策略;    3 
                                                 3
        如果(高级){                            3
            washingStrategy = premiumWash;
        } 别的 {
            洗涤策略=标准洗涤;
        }

        洗涤策略(汽车);                    4个
    }
}
class Car {
    /* Represents a car */
}

type WashingStrategy = (car: Car) => void;       1

function standardWash(car: Car): void {          2
    /* Perform standard wash */
}

function premiumWash(car: Car): void {           2
    /* Perform premium wash */
}

class CarWash {
    service(car: Car, premium: boolean) {
        let washingStrategy: WashingStrategy;    3
                                                 3
        if (premium) {                           3
            washingStrategy = premiumWash;
        } else {
            washingStrategy = standardWash;
        }

        washingStrategy(car);                    4
    }
}

  • 1 WashingStrategy 是一个接受 Car 并返回 void 的函数。
  • 1 WashingStrategy is a function that takes a Car and returns void.
  • 2 standardWash() 和 premiumWash() 实现我们的洗车逻辑。
  • 2 standardWash() and premiumWash() implement our car-washing logic.
  • 3 现在我们可以在选择算法的时候直接给washingStrategy赋一个函数。
  • 3 Now we can assign a function directly to washingStrategy when we select the algorithm.
  • 4 因为washingStrategy变量是一个函数,我们可以简单的调用它。
  • 4 Because the washingStrategy variable is a function, we can simply call it.

如图 5.2所示,此实现比前一个实现的部分更少。

This implementation has fewer parts than the preceding one, as we can see in figure 5.2.

图 5.2。由使用函数的 a组成的策略模式ContextconcreteStrategy1()要么concreteStrategy2()

让我们放大函数类型声明,因为我们是第一次使用它。

Let’s zoom in on the function type declaration, because we’re using one for the first time.

5.1.2.类型功能

5.1.2. Typing functions

该函数standardWash()接受一个类型的参数Car并返回void,因此它的类型是从 Car 到 void 的函数,或者在 TypeScript 语法中,(car: Car) => void。function premiumWash(),即使它有不同的实现,也有完全相同的参数类型和返回类型,所以它有相同的类型。

The function standardWash() takes an argument of type Car and returns void, so its type is function from Car to void or, in TypeScript syntax, (car: Car) => void. The function premiumWash(), even though it has a different implementation, has exactly the same argument type and return type, so it has the same type.

函数类型或签名

函数的类型由其参数类型和返回类型给出。如果两个函数采用相同的参数并返回相同的类型,则它们具有相同的类型。参数集加上返回类型也称为函数的 签名。

The type of a function is given by the type of its arguments and its return type. If two functions take the same arguments and return the same type, they have the same type. The set of arguments plus return type is also known as the signature of a function.

我们想引用这个类型,所以我们通过声明给它一个名字type WashingStrategy = (car: Car) => void。每当我们使用WashingStrategyas 类型时,我们指的是函数类型(car: Car) => void。我们在方法中引用它CarWash.service()

We want to refer to this type, so we give it a name by declaring type WashingStrategy = (car: Car) => void. Whenever we use WashingStrategy as a type, we mean the function type (car: Car) => void. We refer to it in the CarWash.service() method.

因为我们可以键入函数,所以我们可以有代表函数的变量。在我们的例子中,washingStrategy变量代表一个带有我们刚刚命名的签名的函数。我们可以分配任何接受 aCar并返回void给这个变量的函数。我们也可以像函数一样调用它。在第一个使用接口的示例中IWashingStrategy,我们通过调用washingStrategy.wash(car). 在我们的第二个例子中,其中washingStrategy是一个函数,我们简单地调用了washingStrategy(car).

Because we can type functions, we can have variables that represent functions. In our example, the washingStrategy variable represents a function with the signature we just named. We can assign any function that takes a Car and returns void to this variable. We can also call it as we would a function. In the first example that used an IWashingStrategy interface, we ran our car-washing logic by calling washingStrategy.wash(car). In our second example, in which washingStrategy is a function, we simply called washingStrategy(car).

一流的功能

将函数分配给变量并像类型系统中的任何其他值一样对待它们的能力导致了所谓的 一流函数。这意味着该语言将函数视为一等公民,赋予它们与其他值相同的权利:它们有类型;它们可以分配给变量并作为参数传递,检查有效性,并转换(如果兼容)为其他类型。

The ability to assign functions to variables and treat them like any other values in the type system results in what are called first-class functions. That means the language treats functions like first-class citizens, granting them the same rights as other values: they have types; and they can be assigned to variables and passed around as arguments, checked for validity, and converted (if compatible) to other types.

5.1.3.战略实施

5.1.3. Strategy implementations

早些时候,我们看到了两种实现策略模式的方法。对比这两个实现,第一个例子中的书本策略实现需要很多额外的机制:我们需要声明一个接口,我们需要有多个实现该接口的类来提供策略的具体逻辑。第二种实现归结为我们要实现的本质:我们有两个实现逻辑的函数,我们直接引用它们。

Earlier, we saw two ways to implement a strategy pattern. Contrasting the two implementations, the by-the-book strategy implementation in the first example requires a lot of extra machinery: we need to declare an interface, and we need to have multiple classes implementing that interface to provide the concrete logic of the strategy. The second implementation is boiled down to the essence of what we are trying to achieve: we have two functions implementing the logic, and we refer to them directly.

两种实现实现相同的目标。依赖接口的第一种之所以更为普遍,是因为当设计模式在 20 世纪 90 年代风靡一时时,并不是所有的主流编程语言都支持一阶函数。事实上,他们中很少有人这样做。这已不再是这种情况。大多数语言现在都可以键入函数,我们可以利用这种能力来提供一些设计模式的更简洁的实现。

Both implementations achieve the same goals. The reason why the first one, which relies on interfaces, is more widespread is that when design patterns became all the rage in the 1990s, not all mainstream programming languages supported first-order functions. In fact, few of them did. This is no longer the case. Most languages can type functions now, and we can leverage that capability to provide more-succinct implementations of some design patterns.

重要的是要记住模式是相同的:我们仍在封装一系列算法并在运行时选择要使用的算法。不同之处在于实施,现代功能使我们能够更轻松地表达。我们将一个接口和两个类(每个类实现一个方法)替换为一个类型声明和两个函数。

It’s important to keep in mind that the pattern is the same: we are still encapsulating a family of algorithms and selecting at run time the one to use. The difference is in the implementation, which modern capabilities allow us to express more easily. We’re replacing an interface and two classes (each class implementing a method) with a type declaration and two functions.

在大多数情况下,更简洁的实现就足够了。当算法不能表示为简单函数时,我们可能需要重新考虑接口和类的实现。有时,我们需要多个功能或需要跟踪某些状态,在这种情况下,第一个实现会更适合,因为它将策略的相关部分分组在一个通用类型下。

In most cases, the more-succinct implementation is enough. We might need to reconsider the interface and classes implementation when the algorithms are not representable as simple functions. Sometimes, we need multiple functions or need to track some state, in which case the first implementation would be better suited, as it groups the related pieces of a strategy under a common type.

5.1.4.一流的功能

5.1.4. First-class functions

在我们继续之前,让我们快速回顾一下本节中介绍的一些术语:

Before we move on, let’s quickly review some of the terms introduced in this section:

  • 参数集加上函数的返回值称为签名函数的以下两个函数具有相同的签名:
    函数添加(x:数字,y:数字):数字{
        返回 x + y;
    }
    
    函数减法(x:数字,y:数字):数字{
        返回 x - y;
    }
  • The set of arguments plus the return value of a function is called the signature of a function. The following two functions have the same signature:
    function add(x: number, y: number): number {
        return x + y;
    }
    
    function subtract(x: number, y: number): number {
        return x - y;
    }
  • 函数的签名等同于它在可以键入函数的语言中的类型。前面两个函数的类型函数是从 (number, number) 到 number , or (x: number, y: number) => number。请注意,参数的实际名称无关紧要;(a: number, b: number) => number与 具有相同的类型(x: number, y: number) => number
  • The signature of a function is equivalent to its type in languages that can type functions. The preceding two functions have the type function from (number, number) to number, or (x: number, y: number) => number. Note that the actual name of the arguments doesn’t matter; (a: number, b: number) => number has the same type as (x: number, y: number) => number.
  • 当语言像对待任何其他值一样对待函数时,我们说它们支持一等函数。函数可以分配给变量,作为参数传递,并像其他值一样使用,这使代码更具表现力。
  • When languages treat functions as they do any other values, we say that they support first-class functions. Functions can be assigned to variables, passed as arguments, and used like other values, which makes code more expressive.

5.1.5.练习

5.1.5. Exercises

1个

isEven()接受数字作为参数并true在数字为偶数时返回的函数的类型是什么false

  1. [number, boolean]
  2. (x: number) => boolean
  3. (x: number, isEven: boolean)
  4. {x: number, isEven: boolean}

1

What is the type of a function isEven() that takes a number as an argument and returns true if the number is even and false otherwise?

  1. [number, boolean]
  2. (x: number) => boolean
  3. (x: number, isEven: boolean)
  4. {x: number, isEven: boolean}

2个

check()接受一个数字和一个相同类型的函数作为isEven()参数并返回将给定函数应用于给定值的结果的函数 的类型是什么?

  1. (x: number, func: number) => boolean
  2. (x: number) => (x: number) => boolean
  3. (x: number, func: (x: number) => boolean) => boolean
  4. (x: number, func: (x: number) => boolean) => void

2

What is the type of a function check() that takes a number and a function of the same type as isEven() as arguments and returns the result of applying the given function to the given value?

  1. (x: number, func: number) => boolean
  2. (x: number) => (x: number) => boolean
  3. (x: number, func: (x: number) => boolean) => boolean
  4. (x: number, func: (x: number) => boolean) => void

5.2. 没有 switch 语句的状态机

5.2. A state machine without switch statements

一等函数的一个非常有用的应用使我们能够将类的属性定义为具有函数类型。然后我们可以为它分配不同的功能,在运行时改变行为。这作为类的插件方法,我们可以根据需要交换它。

One very useful application of first-class functions enables us to define a property of a class as having a function type. Then we can assign different functions to it, changing the behavior at run time. This acts as a plug-in method on the class, and we can swap it as needed.

Greeter例如,我们可以实现一个可插入的。我们不是实现greet()方法,而是实现greet具有函数类型的属性。然后我们可以给它分配多个问候函数,比如sayGoodMorning()sayGoodNight()

We can implement a pluggable Greeter, for example. Instead of implementing a greet() method, we implement a greet property with a function type. Then we can assign multiple greeting functions to it, such as sayGoodMorning() and sayGoodNight().

清单 5.3。可插拔Greeter
函数 sayGoodMorning(): void {            1
    console.log("早上好!");
}

函数 sayGoodNight(): void {              1
    console.log("晚安!");
}

类迎宾{
    问候语:() => void = sayGoodMorning;     2个
}

let greeter: Greeter = new Greeter();

问候语。问候语();                            3个

greeter.greet = sayGoodNight;               4个

问候语。问候语();                            5个
function sayGoodMorning(): void {           1
    console.log("Good morning!");
}

function sayGoodNight(): void {             1
    console.log("Good night!");
}

class Greeter {
    greet: () => void = sayGoodMorning;     2
}

let greeter: Greeter = new Greeter();

greeter.greet();                            3

greeter.greet = sayGoodNight;               4

greeter.greet();                            5

  • 1 将各自的问候语输出到控制台的两个问候功能
  • 1 Two greeting functions that output their respective greetings to the console
  • 2 greet 是一个没有参数的函数,它返回 void 并且默认为 sayGoodMorning()。
  • 2 greet is a function with no arguments that returns void and defaults to sayGoodMorning().
  • 3 因为greet是一个函数属性,我们可以把它当作类的一个方法来调用。
  • 3 Because greet is a function property, we can call it as a method of the class.
  • 4 我们可以给它分配另一个功能。
  • 4 We can assign another function to it.
  • 5 第二次调用将调用 sayGoodNight()。
  • 5 The second call will invoke sayGoodNight().

这遵循上一节中讨论的策略模式实现,但值得注意的是,这种方法使我们能够轻松地将可插入行为添加到类中。如果我们想添加一个新的问候语,我们只需要添加另一个具有相同签名的函数并将其分配给该greet属性。

This follows from the strategy pattern implementation discussed in the previous section, but it’s worth noting that this approach enables us to easily add pluggable behavior to a class. If we want to add a new greeting, we simply need to add another function with the same signature and assign it to the greet property.

5.2.1.早期的类型编程

5.2.1. Early Programming with Types

在撰写本书的初稿时,我编写了一个小脚本来帮助我保持源代码与文本的同步。草稿是用流行的 Markdown 格式编写的。我将源代码保存在单独的 TypeScript 文件中,这样我就可以编译它们并确保即使我更新了代码示例,它们仍然可以工作。

While working on an early draft of this book, I wrote a small script to help me keep the source code in sync with the text. The draft was written in the popular Markdown format. I kept the source code in separate TypeScript files so I could compile them and ensure that even if I update the code samples, they’ll still work.

我需要一种方法来确保 Markdown 文本始终包含最新的代码示例。代码示例始终出现在包含 的行```ts和包含 的行之间```。当 HTML 从 Markdown 源生成时,```ts被解释为 TypeScript 代码块的开始,它使用 TypeScript 语法高亮显示,同时 ```标记该代码块的结尾。这些代码块的内容必须从我可以在文本之外编译和验证的实际 TypeScript 源文件中内联(图 5.3)。

I needed a way to ensure that the Markdown text always contains the latest code samples. The code samples always appear between a line containing ```ts and a line containing ```. When HTML is generated from the Markdown source, ```ts is interpreted as the beginning of a TypeScript code block, which gets rendered with TypeScript syntax highlighting, whereas ``` marks the end of that code block. The contents of these code blocks had to be inlined from actual TypeScript source files that I could compile and validate outside the text (figure 5.3).

图 5.3。两个 TypeScript (.ts) 文件,其中包含应内联在 Markdown 文档中的代码示例,位于```ts```标记之间。注释<!-- ... -->注释了我的脚本的代码示例。

为了确定哪个代码示例去了哪里,我依靠了一个小技巧。Markdown 允许在文档文本中使用原始 HTML,所以我用HTML 注释,例如<!-- sample1 -->. HTML 注释不会被渲染,所以当 Markdown 转换为 HTML 时,它们变得不可见。另一方面,我的脚本可以使用这些注释来确定在何处内联哪个代码示例。

To determine which code sample went where, I relied on a small trick. Markdown allows raw HTML in the document text, so I annotated each code sample with an HTML comment, such as <!-- sample1 -->. HTML comments do not get rendered, so when Markdown is converted to HTML, they became invisible. On the other hand, my script could use these comments to determine which code sample to inline where.

当所有代码示例都从磁盘加载后,我必须处理草稿的每个 Markdown 文档并生成更新版本,如下所示:

When all code samples were loaded from disk, I had to process each Markdown document of the draft and produce an updated version as follows:

  • 在文本处理模式下,只需将输入文本的每一行按原样复制到输出文档。当遇到标记(<!-- sample -->)时,抓取相应的代码示例,并切换到标记处理模式。
  • In text processing mode, simply copy each line of the input text to the output document as is. When a marker is encountered (<!-- sample -->), grab the corresponding code sample, and switch to marker processing mode.
  • 在标记处理模式下,再次将输入文本的每一行复制到输出文档,直到遇到代码块标记 ( ```ts)。当遇到代码标记时,输出从 TypeScript 文件加载的最新版本的代码示例,并切换到代码处理模式。
  • In marker processing mode, again copy each line of the input text to the output document until we encounter a code block marker (```ts). When the code marker is encountered, output the latest version of the code sample as loaded from the TypeScript file, and switch to code processing mode.
  • 在代码处理模式下,我们已经确保了最新版本的代码在输出文档中,因此我们可以跳过代码块中可能过时的版本。我们跳过每一行,直到遇到代码块结束标记 ( ```)。然后我们切换回文本处理模式。
  • In code processing mode, we already ensured that the latest version of the code is in the output document, so we can skip the potentially outdated version in the code block. We skip each line until we encounter the end of code block marker (```). Then we switch back to text processing mode.

每次运行时,文档中前面带有<!-- ... -->标记的现有代码示例都会更新为磁盘上最新版本的 TypeScript 文件。前面没有的其他代码块<!-- ... -->不会更新,因为它们是在文本处理模式下处理的。

With each run, the existing code samples in the document preceded by a <!-- ... --> marker get updated to the latest version of the TypeScript files on disk. Other code blocks that aren’t preceded by <!-- ... --> don’t get updated, as they are processed in text processing mode.

例如,这里有一个 helloWorld.ts 代码示例。

As an example, here is a helloWorld.ts code sample.

清单 5.4。你好世界.ts
console.log("你好世界!");
console.log("Hello world!");

我们想将此代码嵌入到 Chapter1.md 中并确保它保持最新,如下一个清单所示。

We want to embed this code in Chapter1.md and make sure that it’s kept up to date, as shown in the next listing.

清单 5.5。第一章.md
# 第1章

打印“Hello world!”。
<!-- 你好世界 -->
```ts
console.log("你好");      1``` 
_
# Chapter 1

Printing "Hello world!".
<!-- helloWorld -->
```ts
console.log("Hello");      1
```

  • 1 这不是最新的。这里的字符串是“Hello”,不匹配helloWorld.ts。
  • 1 This is not quite up to date. The string here is “Hello”, which does not match helloWorld.ts.

该文档逐行处理如下:

This document gets processed line by line as follows:

  1. 在文本处理模式下,"# Chapter 1"按原样复制到输出。
  2. In text processing mode, "# Chapter 1" is copied to the output as is.
  3. ""(空行)按原样复制到输出。
  4. "" (blank line) is copied to the output as is.
  5. "Printing "Hello world!"."按原样复制到输出。
  6. "Printing "Hello world!"." is copied to the output as is.
  7. "<!-- helloWorld -->"按原样复制到输出。不过,这是一个标记,因此我们跟踪要内联的代码示例 (helloWorld.ts) 并切换到标记处理模式。
  8. "<!-- helloWorld -->" is copied to the output as is. This is a marker, though, so we keep track of the code sample to be inlined (helloWorld.ts) and switch to marker processing mode.
  9. "```ts"按原样复制到输出。这个标记是一个代码块标记,所以在复制到输出后,我们也立即输出了helloWorld.ts的内容。我们还切换到代码处理模式。
  10. "```ts" is copied to the output as is. This marker is a code block marker, so immediately after copying it to the output, we also output the contents of helloWorld.ts. We also switch to code processing mode.
  11. "console.log("Hello");"被跳过。我们不会在代码处理模式下复制行,因为我们正在用代码示例文件中的最新内容替换它们。
  12. "console.log("Hello");" is skipped. We don’t copy lines in code processing mode, as we are replacing them with the latest in the code sample file.
  13. "```"是代码块结束标记。我们插入它然后切换回文本处理模式。
  14. "```" is an end-of-code-block marker. We insert it and then switch back to text processing mode.

5.2.2.状态机

5.2.2. State machines

我们的文本处理脚本的行为最好建模为状态机。状态机具有一组状态和一组状态对之间的转换。机器以给定状态启动,也称为启动状态;如果满足某些条件,它可以转换到另一个状态。

The behavior of our text processing script is best modeled as a state machine. A state machine has a set of states and a set of transitions between pairs of states. The machine starts in a given state, also known as the start state; if certain conditions are met, it can transition to another state.

这正是我们的文本处理器使用其三种处理模式所做的。输入行在 文本处理模式下以一定的方式处理。当满足某些条件(<!-- sample -->遇到标记)时,我们的处理器将转换为标记处理模式。同样,当满足某些其他条件(```ts遇到代码块标记)时,它会转换为代码处理模式。当遇到代码块标记的结尾 ( ```) 时,它会转换回文本处理模式图 5.4)。

This is exactly what our text processor does with its three processing modes. Input lines are processed in a certain way in text processing mode. When some condition is met (a <!-- sample --> marker is encountered), our processor transitions to marker processing mode. Again, when some other condition is met (a ```ts code-block marker is encountered), it transitions to code processing mode. When the end of the code-block marker is encountered (```), it transitions back to text processing mode (figure 5.4).

图 5.4。文本处理状态机,具有三种状态(文本处理、标记处理、代码处理)和基于输入的状态之间的转换。文本处理是初始状态或开始状态。

现在我们已经对解决方案进行了建模,让我们看看我们将如何实施它。实现状态机的一种方法是将状态集定义为枚举,跟踪当前状态,并使用涵盖switch所有可能状态的语句获得所需的行为。在我们的例子中,我们可以定义一个TextProcessingMode枚举。

Now that we’ve modeled the solution, let’s look at how we would implement it. One way to implement a state machine is to define the set of states as an enumeration, keeping track of the current state, and get the desired behavior with a switch statement that covers all possible states. In our case, we can define a TextProcessingMode enum.

我们的TextProcessor类将跟踪属性中的当前状态并在方法中mode实现语句。根据状态,此方法将依次调用三种处理方法之一:、或。这些函数将实现文本处理,然后在适当的时候通过更新当前状态转换到另一个状态。 switchprocess-Line()processTextLine()processMarkerLine()process-CodeLine()

Our TextProcessor class will keep track of the current state in a mode property and implement the switch statement in a process-Line() method. Depending on the state, this method will in turn invoke one of the three processing methods: processTextLine(), processMarkerLine(), or process-CodeLine(). These functions will implement the text processing and then, when appropriate, transition to another state by updating the current state.

处理由多行文本组成的 Markdown 文档意味着使用我们的状态机依次处理每一行,然后将最终结果返回给调用者,如下一个清单所示。

Processing a Markdown document consisting of multiple lines of text means processing each line in turn, using our state machine, and then returning the final result to the caller, as shown in the next listing.

清单 5.6。状态机实现
枚举 TextProcessingMode {                              1
    文本,
    标记,
    代码,
}

类 TextProcessor {
    私有模式:TextProcessingMode = TextProcessingMode.Text;
    私有结果:string[] = [];
    私有代码示例:string[] = [];

    processText(行:字符串[]):字符串[] {
        这个.result = [];
        this.mode = TextProcessingMode.Text;

        for (let line of lines) {                      2
            this.processLine(线);
        }

        返回这个结果;
    }

    私人流程线(行:字符串):void {
        开关(this.mode){                           3
            案例 TextProcessingMode.Text:
                this.processTextLine(行);
                休息;
            案例 TextProcessingMode.Marker:
                this.processMarkerLine(线);
                休息;
            案例 TextProcessingMode.Code:
                this.processCodeLine(行);
                休息;
        }
    }

    private processTextLine(line: string): void {      4
        this.result.push(line);

        如果 (line.startsWith("<!--")) {                 4
            this.loadCodeSample(行);

            this.mode = TextProcessingMode.Marker;
        }
    }

    私有 processMarkerLine(line: string): void {    5
        this.result.push(line);

        如果 (line.startsWith("```ts")) {                5
            this.result = this.result.concat(this.codeSample);

            this.mode = TextProcessingMode.Code;
        }
    }

    private processCodeLine(line: string): void {      6 
        if (line.startsWith("```")) {                  6
            this.result.push(line);

            this.mode = TextProcessingMode.Text;
        }
    }

    私人 loadCodeSample(行:字符串){             7
        /* 根据marker加载sample,存入this.codeSample */
    }
}
enum TextProcessingMode {                             1
    Text,
    Marker,
    Code,
}

class TextProcessor {
    private mode: TextProcessingMode = TextProcessingMode.Text;
    private result: string[] = [];
    private codeSample: string[] = [];

    processText(lines: string[]): string[] {
        this.result = [];
        this.mode = TextProcessingMode.Text;

        for (let line of lines) {                     2
            this.processLine(line);
        }

        return this.result;
    }

    private processLine(line: string): void {
        switch (this.mode) {                          3
            case TextProcessingMode.Text:
                this.processTextLine(line);
                break;
            case TextProcessingMode.Marker:
                this.processMarkerLine(line);
                break;
            case TextProcessingMode.Code:
                this.processCodeLine(line);
                break;
        }
    }

    private processTextLine(line: string): void {     4
        this.result.push(line);

        if (line.startsWith("<!--")) {                4
            this.loadCodeSample(line);

            this.mode = TextProcessingMode.Marker;
        }
    }

    private processMarkerLine(line: string): void {   5
        this.result.push(line);

        if (line.startsWith("```ts")) {               5
            this.result = this.result.concat(this.codeSample);

            this.mode = TextProcessingMode.Code;
        }
    }

    private processCodeLine(line: string): void {     6
        if (line.startsWith("```")) {                 6
            this.result.push(line);

            this.mode = TextProcessingMode.Text;
        }
    }

    private loadCodeSample(line: string) {            7
        /* Load sample based on marker, store in this.codeSample  */
    }
}

  • 1 状态表示为枚举。
  • 1 States are represented as an enum.
  • 2 处理文本文档意味着处理每一行并返回结果字符串数组。
  • 2 Processing a text document means processing each line and returning the resulting string array.
  • 3 状态机switch语句根据当前状态调用合适的处理器。
  • 3 The state machine switch statement calls the appropriate processor based on the current state.
  • 4 处理一行文本。如果该行以“<!--”开头,则加载代码示例并转换到下一个状态。
  • 4 Processes a line of text. If the line starts with “<!--”, load code sample and transition to next state.
  • 5 进程标记。如果该行以“```ts”开头,则内联代码示例并过渡到下一个状态。
  • 5 Processes marker. If the line starts with “```ts”, inline code sample and transition to next state.
  • 6 通过跳行处理代码。如果该行以“```”开头,则转换到文本处理状态。
  • 6 Process code by skipping lines. If the line starts with “```”, transition to text processing state.
  • 7 这个函数的主体被省略了,因为它对于这个例子来说并不重要。
  • 7 The body of this function is omitted, as it’s not important for this example.

我们省略了从外部文件加载示例的代码,因为它与我们的状态机讨论并不特别相关。此实现有效,但如果我们使用可插入功能,则可以简化。

We omitted the code to load a sample from an external file, as it isn’t particularly relevant to our state machine discussion. This implementation works but can be simplified if we use a pluggable function.

请注意,我们所有的文本处理函数都具有相同的签名:它们将一行文本作为参数string并返回void。如果我们不是processLine()实现一个大switch语句并转发到适当的函数,而是创建processLine()这些函数之一呢?

Note that all our text processing functions have the same signature: they take a line of text as a string argument and return void. What if, instead of having processLine() implement a big switch statement and forward to the appropriate function, we make processLine() one of those functions?

processLine()我们可以将其定义为具有 type 的类的属性,(line: string) => void并使用 对其进行初始化,而不是作为方法实现process-TextLine(),如以下代码所示。mode然后,在三种文本处理方法中的每一种中,我们都设置为不同的processLine方法,而不是设置为不同的枚举值。事实上,我们不再需要从外部跟踪我们的状态。我们甚至不需要枚举!

Instead of implementing processLine() as a method, we can define it as a property of the class with type (line: string) => void and initialize it with process-TextLine(), as shown in the following code. Then, in each of the three text processing methods, instead of setting mode to a different enum value, we set processLine to a different method. In fact, we no longer need to keep track of our state externally. We don’t even need an enum!

清单 5.7。替代状态机实现
类 TextProcessor {
    私有结果:string[] = [];
    private processLine: (line: string) => void = this.processTextLine;
    私有代码示例:string[] = [];

    processText(行:字符串[]):字符串[] {
        这个.result = [];
        this.processLine = this.processTextLine;

        for (let line of lines) {
            this.processLine(线);
        }

        返回这个结果;
    }

    私有 processTextLine(行:字符串):void {
        this.result.push(line);

        如果 (line.startsWith("<!--")) {
            this.loadCodeSample(行);

            this.processLine = this.processMarkerLine;     1个
        }
    }

    私有 processMarkerLine(行:字符串):void {
        this.result.push(line);

        如果 (line.startsWith("```ts")) {
            this.result = this.result.concat(this.codeSample);

            this.processLine = this.processCodeLine;       1个
        }
    }

    私有 processCodeLine(行:字符串):void {
        如果(line.startsWith(“```”)){
            this.result.push(line);

            this.processLine = this.processTextLine;       1个
        }
    }

    私有 loadCodeSample(行:字符串){
        /* 根据marker加载sample,存入this.codeSample */
    }
}
class TextProcessor {
    private result: string[] = [];
    private processLine: (line: string) => void = this.processTextLine;
    private codeSample: string[] = [];

    processText(lines: string[]): string[] {
        this.result = [];
        this.processLine = this.processTextLine;

        for (let line of lines) {
            this.processLine(line);
        }

        return this.result;
    }

    private processTextLine(line: string): void {
        this.result.push(line);

        if (line.startsWith("<!--")) {
            this.loadCodeSample(line);

            this.processLine = this.processMarkerLine;    1
        }
    }

    private processMarkerLine(line: string): void {
        this.result.push(line);

        if (line.startsWith("```ts")) {
            this.result = this.result.concat(this.codeSample);

            this.processLine = this.processCodeLine;      1
        }
    }

    private processCodeLine(line: string): void {
        if (line.startsWith("```")) {
            this.result.push(line);

            this.processLine = this.processTextLine;      1
        }
    }

    private loadCodeSample(line: string) {
        /* Load sample based on marker, store in this.codeSample  */
    }
}

  • 1 现在通过将 this.processLine 更新为适当的方法来完成状态转换。
  • 1 State transitions are now done by updating this.processLine to the appropriate method.

第二种实现摆脱了TextProcessingMode枚举、mode属性和switch将处理转发到适当方法的语句。而不是处理转发,processLine现在合适的处理方式。

The second implementation gets rid of the TextProcessingMode enum, the mode property, and the switch statement that forwarded processing to the appropriate method. Instead of handling forwarding, processLine now is the appropriate processing method.

此实现消除了单独跟踪状态并使其与处理逻辑保持同步的需要。如果我们想引入一个新的状态,旧的实现会迫使我们更新几个地方的代码。除了实现新的处理逻辑和状态转换之外,我们还必须更新枚举并向语句中添加另一个 case switch。我们的替代实现消除了对该任务的需求:状态完全由函数表示。

This implementation removes the need to keep track of states separately and keep that in sync with the processing logic. If we ever wanted to introduce a new state, the old implementation would’ve forced us to update the code in several places. Besides implementing the new processing logic and state transitions, we would’ve had to update the enum and add another case to the switch statement. Our alternative implementation removes the need for that task: a state is represented purely by a function.

具有求和类型的状态机

需要注意的是,对于具有许多状态的状态机,显式捕获状态甚至转换可能会使代码更易于理解。尽管如此,switch另一种可能的实现不是使用枚举和语句,而是将每个状态表示为一个单独的类型,并将整个状态机表示为可能状态的总和类型,从而允许我们将其分解为类型安全的组件。以下是我们如何使用求和类型实现状态机的示例。代码有点冗长,所以如果可能的话,我们应该尝试我们目前讨论的实现,这是基于 的switch状态机的另一种替代方法。

One caveat is that for state machines with many states, capturing states and even transitions explicitly might make the code easier to understand. Even so, instead of using enums and switch statements, another possible implementation represents each state as a separate type and the whole state machine as a sum type of the possible states, allowing us to break it apart into type-safe components. Following is an example of how we would implement the state machine by using a sum type. The code is a bit more verbose, so if possible, we should try the implementation we’ve discussed so far, which is another alternative to a switch-based state machine.

当使用求和类型时,每个状态由不同的类型表示,因此我们有 a TextLineProcessor、 aMarkerLineProcessor和 a CodeLine-Processor。每个都跟踪成员中到目前为止已处理的行result,并提供一种process()方法来处理一行文本。

When a sum type is used, each state is represented by a different type, so we have a TextLineProcessor, a MarkerLineProcessor, and a CodeLine-Processor. Each keeps track of the processed lines so far in a result member and provides a process() method to handle a line of text.

求和型状态机

State machine with sum type

类 TextLineProcessor {
    结果:字符串[];

    构造函数(结果:字符串[]){
        this.result = 结果;
    }

    过程(行:字符串):TextLineProcessor | 标记线处理器 {   1
        this.result.push(line);

        如果 (line.startsWith("<!--")) {                                 2
            返回新的标记线处理器(
                this.result, this.loadCodeSample(line));
        } 别的 {
            归还这个;
        }
    }

    private loadCodeSample(line: string): string[] {
        /* 根据marker加载sample,存入this.codeSample */
    }
}

类 MarkerLineProcessor {
    结果:字符串[];
    代码示例:字符串[]

    构造函数(结果:字符串[],代码示例:字符串[]){
        this.result = 结果;
        this.codeSample = codeSample;
    }

    过程(行:字符串):MarkerLineProcessor | 代码线处理器 { 3
        this.result.push(line);

        如果 (line.startsWith("```ts")) {                               4
            this.result = this.result.concat(this.codeSample);

            返回新的 CodeLineProcessor(this.result);
        } 别的 {
            归还这个;
        }
    }
}

类 CodeLineProcessor {
    结果:字符串[];

    构造函数(结果:字符串[]){
        this.result = 结果;
    }

    过程(行:字符串):CodeLineProcessor | TextLineProcessor {    5 
        if (line.startsWith("```")) {                                 6
            this.result.push(line);

            返回新的 TextLineProcessor(this.result);
        } 别的 {
            归还这个;
        }
    }
}

函数 processText(行:字符串):字符串 [] {
    让处理器:TextLineProcessor | 标记线处理器           7
        | CodeLineProcessor = new TextLineProcessor([]);

    for (let line of lines) {
        processor = processor.process(line);                         8个
    }

    返回处理器.result;
}
class TextLineProcessor {
    result: string[];

    constructor(result: string[]) {
        this.result = result;
    }

    process(line: string): TextLineProcessor | MarkerLineProcessor {  1
        this.result.push(line);

        if (line.startsWith("<!--")) {                                2
            return new MarkerLineProcessor(
                this.result, this.loadCodeSample(line));
        } else {
            return this;
        }
    }

    private loadCodeSample(line: string): string[] {
        /* Load sample based on marker, store in this.codeSample */
    }
}

class MarkerLineProcessor {
    result: string[];
    codeSample: string[]

    constructor(result: string[], codeSample: string[]) {
        this.result = result;
        this.codeSample = codeSample;
    }

    process(line: string): MarkerLineProcessor | CodeLineProcessor { 3
        this.result.push(line);

        if (line.startsWith("```ts")) {                              4
            this.result = this.result.concat(this.codeSample);

            return new CodeLineProcessor(this.result);
        } else {
            return this;
        }
    }
}

class CodeLineProcessor {
    result: string[];

    constructor(result: string[]) {
        this.result = result;
    }

    process(line: string): CodeLineProcessor | TextLineProcessor {   5
        if (line.startsWith("```")) {                                6
            this.result.push(line);

            return new TextLineProcessor(this.result);
        } else {
            return this;
        }
    }
}

function processText(lines: string): string[] {
    let processor: TextLineProcessor | MarkerLineProcessor           7
        | CodeLineProcessor = new TextLineProcessor([]);

    for (let line of lines) {
        processor = processor.process(line);                         8
    }

    return processor.result;
}

  • 1 TextLineProcessor 返回 TextLineProcessor 或 MarkerLineProcessor 以处理下一行。
  • 1 TextLineProcessor returns either a TextLineProcessor or a MarkerLineProcessor to process the next line.
  • 2 如果该行以“<!--”开头,则返回一个新的MarkerLineProcessor;否则,退回该处理器。
  • 2 If the line starts with “<!--”, return a new MarkerLineProcessor; otherwise, return this processor.
  • 3 MarkerLineProcessor 返回 MarkerLineProcessor 或 CodeLineProcessor。
  • 3 MarkerLineProcessor returns either a MarkerLineProcessor or a CodeLineProcessor.
  • 4 如果我们遇到“```ts”,加载代码示例并返回一个新的CodeLineProcessor;否则,退回该处理器。
  • 4 If we encounter “```ts”, load the code sample and return a new CodeLineProcessor; otherwise, return this processor.
  • 5 CodeLineProcessor 返回一个 CodeLineProcessor 或一个 TextLineProcessor。
  • 5 CodeLineProcessor returns a CodeLineProcessor or a TextLineProcessor.
  • 6 如果该行以“```”开头,将其追加到结果中并返回一个新的 TextLineProcessor;否则,退回该处理器。
  • 6 If the line starts with “```”, append it to the result and return a new TextLineProcessor; otherwise, return this processor.
  • 7 状态由处理器表示,它是 TextLineProcessor、MarkerLineProcessor 和 CodeLineProcessor 的总和类型。
  • 7 The states are represented by the processor, which is a sum type of TextLineProcessor, MarkerLineProcessor, and CodeLineProcessor.
  • 8 处理器在每行处理后得到更新,以防状态发生变化。
  • 8 processor gets updated after each line processed, in case there is a state change.

我们所有的处理器都返回一个处理器实例:this,如果没有状态变化,或者一个新的处理器作为状态变化。通过调用每一行文本并通过将其重新分配给方法调用的结果来更新状态更改来 运行processText()状态机。process()processor

All our processors return a processor instance: this, if there is no state change, or a new processor as state changes. The processText() runs the state machine by calling process() on each line of text and updating processor as state changes by reassigning it to the result of the method call.

现在状态集在变​​量的签名中明确说明processor,可以是 a TextLineProcessor、 aMarkerLineProcessor或 a CodeLineProcessor

Now the set of states is spelled out explicitly in the signature of the processor variable, which can be a TextLineProcessor, a MarkerLineProcessor, or a CodeLineProcessor.

可能的转换在方法的签名中被捕获process()TextLineProcessor.process例如,返回TextLineProcessor | MarkerLine-Processor,因此它可以保持相同的状态 ( TextLineProcessor) 或转换到该MarkerLineProcessor状态。如果需要,这些状态类可以有更多的属性和成员。这个实现比依赖函数的实现稍微长一些,所以如果我们不需要额外的特性,我们最好使用更简单的解决方案。

The possible transitions are captured in the signatures of the process() methods. TextLineProcessor.process returns TextLineProcessor | MarkerLine-Processor, for example, so it can stay in the same state (TextLineProcessor) or transition to the MarkerLineProcessor state. These state classes can have more properties and members if needed. This implementation is slightly longer than the one that relies on functions, so if we don’t need the extra features, we are better off using the simpler solution.

5.2.3.状态机实现回顾

5.2.3. State machine implementation recap

让我们快速回顾一下本节中讨论的替代实现,然后看看函数类型的其他应用:

Let’s quickly review the alternative implementations discussed in this section and then look at other applications of function types:

  • 状态机的“经典”实现使用枚举来定义所有可能的状态,使用该枚举类型的变量来跟踪当前状态,并使用大语句来确定应根据当前状态执行哪些处理switch。状态转换是通过更新当前状态变量来实现的。这种实现的缺点是状态从我们要在每个状态期间运行的处理中删除,因此当我们在给定状态下运行错误的处理时,编译器无法防止错误。没有什么能阻止我们,例如,processCodeLine()即使我们在TextProcessingMode.Text. 我们还必须将状态和转换作为一个单独的枚举来维护,但存在不同步的风险。switch(例如, 我们可能会向枚举添加一个新值,但忘记在语句中为其添加 case 。)
  • The “classical” implementation of a state machine uses an enum to define all the possible states, a variable of that enum type to keep track of the current state, and a big switch statement to determine which processing should be performed based on the current state. State transitions are implemented by updating the current state variable. The drawback of this implementation is that states are removed from the processing that we want to run during each state, so the compiler can’t prevent mistakes when we run the wrong processing while in a given state. Nothing stops us, for example, from calling processCodeLine() even when we’re in TextProcessingMode.Text. We also have to maintain state and transitions as a separate enum, with the risk of running out of sync. (We might add a new value to the enum but forget to add a case for it in the switch statement, for example.)
  • 功能实现将每个处理状态表示为一个函数,并依赖于一个函数属性来跟踪当前状态。状态转换是通过将函数属性分配给另一个状态来实现的。此实现是轻量级的,应该适用于许多情况。有两个缺点:有时候,我们需要关联更多的信息 每个州;在声明可能的状态和转换时,我们可能希望明确。
  • The functional implementation represents each processing state as a function and relies on a function property to track the current state. State transitions are implemented by assigning the function property to another state. This implementation is lightweight and should work for many cases. There are two drawbacks: sometimes, we need to associate more information with each state; and we might want to be explicit when declaring the possible states and transitions.
  • 总和类型实现将每个处理状态表示为一个类,并依赖于表示所有可能状态的总和类型的变量来跟踪当前状态。状态转换是通过将变量重新分配给另一个状态来实现的,这允许我们向每个状态添加属性和成员并将它们组合在一起。缺点是代码比功能性替代方案更冗长。
  • The sum type implementation represents each processing state as a class and relies on a variable representing the sum type of all the possible states to keep track of the current state. State transitions are implemented by reassigning the variable to another state, which allows us to add properties and members to each state and keep them grouped together. The drawback is that the code is more verbose than the functional alternative.

我们对状态机的讨论到此结束。在下一节中,我们将了解函数类型的另一种用途:实现惰性求值。

This concludes our discussion of state machines. In the next section, we look at another use of function types: implementing lazy evaluation.

5.2.4.练习

5.2.4. Exercises

1个

对可以是openclosed作为状态机的简单连接建模。连接打开connect和关闭disconnect

1

Model a simple connection that can be open or closed as a state machine. A connection is opened with connect and closed with disconnect.

2个

将前面的连接实现为具有功能的功能状态机process()。在关闭的连接中,process()打开一个连接。在打开的连接中,process()调用read()返回字符串的函数。如果字符串为空,则关闭连接;否则,读取的字符串将记录到控制台。read()给出为declare function read(): string;

2

Implement the preceding connection as a functional state machine with a process() function. In a closed connection, process() opens a connection. In an open connection, process() calls a read() function that returns a string. If the string is empty, the connection is closed; otherwise, the read string is logged to the console. read() is given as declare function read(): string;.

5.3. 避免使用惰性值进行昂贵的计算

5.3. Avoiding expensive computation with lazy values

能够将函数用作任何其他值的另一个优点是我们可以存储它们并仅在需要时调用它们。有时,我们可能想要的值的计算成本很高。假设我的程序可以构建 aBike和 a Car。我可能想要一个Car. 但是 a 的Car建造成本很高,所以我可能会决定骑自行车。A 的Bike建造成本非常低,所以我不担心成本。与其总是在Car每次运行程序时构建一个,只是为了我可以在需要时使用它,让我能够请求一个不是更好吗Car?在那种情况下,我会在真正需要它的时候请求它,然后执行昂贵的构建逻辑。如果我从不要求它,就不会浪费任何资源。

Another advantage of being able to use functions as any other value is that we can store them and invoke them only when needed. Sometimes, a value we may want is expensive to compute. Let’s say that my program can build a Bike and a Car. I may want a Car. But a Car is expensive to build, so I might decide to ride my bike instead. A Bike is extremely cheap to build, so I’m not worried about the cost. Instead of always building a Car with each run of the program, just so I can use it if I want it, wouldn’t it be better to give me the ability to ask for a Car? In that case, I would ask for it when I really needed it and execute the expensive building logic then. If I never asked for it, no resources would be wasted.

这个想法是尽可能推迟昂贵的计算,希望它可能根本不需要。因为计算被表示为函数,所以我们可以传递函数而不是实际值,并在我们需要这些值时调用它们。这个过程称为惰性求值。相反的是急切求值,我们立即产生值并传递它们,即使我们稍后决定丢弃它们也是如此。

The idea is to postpone expensive computation as much as possible, in the hope that it may not be needed at all. Because computation is expressed as functions, we can pass around functions instead of actual values and call them when and whether we need the values. This process is called lazy evaluation. The opposite is eager evaluation, in which we produce the values immediately and pass them around even if we decide later to discard them.

清单 5.8。热切Car生产
自行车类 { }                                                1
类汽车 { }                                                 1

函数 chooseMyRide(bike: Bike, car: Car): Bike | 汽车 {     2
    如果(isItRaining()){
        还车;
    } 别的 {
        归还自行车;
    }
}

选择我的骑行(新自行车(),新汽车());                         3个
class Bike { }                                               1
class Car { }                                                1

function chooseMyRide(bike: Bike, car: Car): Bike | Car {    2
    if (isItRaining()) {
        return car;
    } else {
        return bike;
    }
}

chooseMyRide(new Bike(), new Car());                         3

  • 1 辆 汽车和自行车。让我们假设 Car 的创建成本很高。
  • 1 Car and Bike. Let’s assume that Car is expensive to create.
  • 2 chooseMyRide() 函数将根据某些条件选择自行车或汽车
  • 2 The chooseMyRide() function will pick Bike or Car, depending on some condition
  • 3 要调用 chooseMyRide(),我们需要创建一个 Car。
  • 3 To call chooseMyRide(), we need to create a Car.

在我们急切的Car生产示例中,要调用chooseMyRide(),我们需要提供一个Car对象,所以我们已经支付了构建一个Car. 如果天气好并且我决定骑自行车,那么这个Car实例就是免费创建的。

In our eager Car production example, to call chooseMyRide(), we need to supply a Car object, so we’re already paying the cost of building a Car. If the weather is nice and I decide to ride my bike, the Car instance was created for nothing.

让我们切换到惰性方法。我们不提供 a Car,而是提供一个在调用时返回 a 的函数Car

Let’s switch to a lazy approach. Instead of providing a Car, let’s provide a function that returns a Car when called.

清单 5.9。惰性Car生产
类自行车{}
类车{}

function chooseMyRide(bike: Bike, car: () => Car ): 自行车 | 汽车 {    1
    如果(isItRaining()){
        返回汽车() ;                                             2个
    } 别的 {
        归还自行车;
    }
}

函数 makeCar(): 汽车 {                                          3
    返回新车();
}

chooseMyRide(新自行车(), makeCar );                                3个
class Bike { }
class Car { }

function chooseMyRide(bike: Bike, car: () => Car): Bike | Car {   1
    if (isItRaining()) {
        return car();                                             2
    } else {
        return bike;
    }
}

function makeCar(): Car {                                         3
    return new Car();
}

chooseMyRide(new Bike(), makeCar);                                3

  • 1 chooseMyRide() 现在采用返回 Car 的函数,而不是 Car 参数。
  • 1 Instead of a Car argument, chooseMyRide() now takes a function that returns a Car.
  • 2 仅当我们确定需要汽车时才调用此函数。
  • 2 We call this function only when we know for sure that we need a Car.
  • 3 我们将 car-making 包装在一个函数中,并将其传递给 chooseMyRide()。
  • 3 We wrap car-making in a function and pass that to chooseMyRide().

Car除非确实需要,否则惰性版本不会产生昂贵的开销。如果我决定改为骑自行车,则该函数永远不会被调用,也不会Car被创建。

The lazy version will not create an expensive Car unless it’s really needed. If I decide to ride my bike instead, the function never gets called, and no Car gets created.

这是我们可以通过纯面向对象的构造来实现的,尽管需要更多的代码。我们可以声明一个CarFactory包装方法的类makeCar(),并将其用作chooseMyRide(). CarFactory然后我们将在调用时创建一个新实例chooseMyRide(),该实例将调用需要时的方法。但是,当我们可以写得更少时,为什么还要写更多的代码呢?事实上,我们可以让我们的代码更短。

This is something we could achieve with pure object-oriented constructs, albeit with a lot more code. We could declare a CarFactory class that wraps a makeCar() method and use that as the argument to chooseMyRide(). We would then create a new instance of CarFactory when calling chooseMyRide(), which would invoke the method when needed. But why write more code when we can write less? In fact, we can make our code even shorter.

5.3.1.拉姆达斯

5.3.1. Lambdas

大多数现代编程语言都支持匿名函数lambda。Lambda 类似于普通函数,但没有名称。每当我们有一个一次性函数时,我们都会使用 lambdas:一个我们通常只引用一次的函数,因此为它命名的麻烦变成了额外的工作。相反,我们可以提供内联实现。

Most modern programming languages support anonymous functions, or lambdas. Lambdas are similar to normal functions but don’t have names. We would use lambdas whenever we have a one-off function: a function we usually refer to only once, so going through the trouble of naming it becomes extra work. Instead, we can provide an inline implementation.

在我们的惰性汽车示例中,一个很好的候选者是makeCar()。因为chooseMyRide()需要一个不带参数且返回 a 的函数Car,所以我们必须声明这个我们只引用一次的新函数:当我们将它作为参数传递给chooseMyRide(). 我们可以使用匿名函数代替此函数,如以下清单所示。

In our lazy car example, a good candidate is makeCar(). Because chooseMyRide() needs a function with no arguments that returns a Car, we had to declare this new function that we refer to only once: when we pass it as an argument to chooseMyRide(). Instead of this function, we can use an anonymous function, as shown in the following listing.

清单 5.10。匿名Car生产
类自行车{}
类车{}

函数 chooseMyRide(bike: Bike, car: () => Car): Bike | 车 {
    如果(isItRaining()){
        回车();
    } 别的 {
        归还自行车;
    }
}

chooseMyRide(new Bike(), () => new Car() );    1个
class Bike { }
class Car { }

function chooseMyRide(bike: Bike, car: () => Car): Bike | Car {
    if (isItRaining()) {
        return car();
    } else {
        return bike;
    }
}

chooseMyRide(new Bike(), () => new Car());    1

  • 1 不接受任何参数并返回 Car 的 lambda
  • 1 A lambda that doesn’t take any arguments and returns a Car

TypeScript lambda 语法与函数类型声明非常相似:我们在括号中有参数列表(在这种特殊情况下没有参数),然后是=>函数体。如果函数有多行,我们会把它们放在{and之间},,但是因为我们只有一次调用new Car(),这被隐式地认为是 lambda 的返回语句,所以我们去掉makeCar()构造逻辑并将其放在单线。

The TypeScript lambda syntax is very similar to the function type declaration: we have the list of arguments (none in this particular case) in parentheses, then =>, and then the body of the function. If the function had multiple lines, we would’ve put them between { and }, but because we have only a single call to new Car(), this is implicitly considered to be the return statement for the lambda, so we get rid of makeCar() and put the construction logic in a one-liner.

Lambda 或匿名函数

lambda 或匿名函数是没有名称的函数定义。Lambda 通常用于一次性的、短暂的处理,并像数据一样传递。

A lambda, or anonymous function, is a function definition that doesn’t have a name. Lambdas are usually used for one-off, short-lived processing and are passed around like data.

如果我们不能输入函数,Lambdas 就不会很有用。我们将如何处理诸如这样的表达式() => new Car()?如果我们不能将它存储在一个变量中或将它作为参数传递给另一个函数,那么它真的没有多大用处。另一方面,具有像值一样传递函数的能力可以实现像前面那样的场景,在这种情况下,Car懒惰地生成一个实例只比急切的版本长几个字符。

Lambdas wouldn’t be very useful if we were unable to type functions. What would we do with an expression such as () => new Car()? If we couldn’t store it in a variable or pass it as an argument to another function, there really wouldn’t be much use for it. On the other hand, having the ability to pass functions around like values enables scenarios like the preceding one, in which producing a Car instance lazily is just a few characters longer than the eager version.

惰性评估

许多函数式编程语言的一个共同特征是惰性求值。在这样的语言中,一切都尽可能晚地评估,我们不必明确说明。在这样的语言中, chooseMyRide()默认情况下既不会构造 aBike也不会构造 a Car。只有当我们实际尝试使用返回的对象时chooseMyRide()——ride()例如,通过调用它——才会创建 Bikeor 。Car

A common feature of many functional programming languages is lazy evaluation. In such languages, everything is evaluated as late as possible, and we don’t have to be explicit about it. In such languages, chooseMyRide() would by default construct neither a Bike nor a Car. Only when we actually try to use the object returned by chooseMyRide()—by calling ride() on it, for example—would the Bike or Car be created.

TypeScript、Java、C# 和 C++ 等命令式编程语言受到热切的评估。话虽这么说,正如我们之前看到的,我们可以在必要时相当容易地模拟惰性求值。当我们稍后讨论生成器时,我们会看到更多这样的例子。

Imperative programming languages such as TypeScript, Java, C#, and C++ are eagerly evaluated. That being said, as we saw previously, we can simulate lazy evaluation fairly easily when necessary. We’ll see more examples of this when we discuss generators later.

5.3.2.锻炼

5.3.2. Exercise

1个

以下哪项实现了将两个数字相加的 lambda?

  1. function add(x: number, y: number)=> number { return x + y; }
  2. add(x: number, y: number) => number { return x + y; }
  3. add(x: number, y: number) { return x + y; }
  4. (x: number, y: number) => x + y;

1

Which of the following implements a lambda that adds two numbers?

  1. function add(x: number, y: number)=> number { return x + y; }
  2. add(x: number, y: number) => number { return x + y; }
  3. add(x: number, y: number) { return x + y; }
  4. (x: number, y: number) => x + y;

5.4. 使用 map、filter 和 reduce

5.4. Using map, filter, and reduce

让我们看一下类型化函数解锁的另一种能力:接受参数或返回其他函数的函数。接受一个或多个非函数参数并返回非函数类型的“普通”函数也称为一阶函数,或常规的普通函数。将一阶函数作为参数或返回一阶函数的函数称为二阶函数

Let’s look at another capability unlocked by typed functions: functions that take as arguments or return other functions. A “normal” function that accepts one or more nonfunction arguments and returns a nonfunction type is also known as a first-order function, or a regular, run-of-the-mill function. A function that takes a first-order function as an argument or returns a first-order function is called a second-order function.

我们可以往上爬,把一个以二阶函数为参数或返回二阶函数的函数称为三阶函数但实际上,我们只是指所有以二阶函数为参数或返回的函数其他函数作为高阶函数

We could climb up the ladder and say that a function that takes a second-order function as an argument or returns a second-order function is called a third-order function, but in practice, we simply refer to all functions that take or return other functions as higher-order functions.

高阶函数的一个例子是chooseMyRide()上一节中的第二次迭代。该函数需要一个 type 的参数() => Car,它本身就是一个函数。

An example of a higher-order function is the second iteration of chooseMyRide() from the preceding section. That function requires an argument of type () => Car, which would be a function itself.

事实上,事实证明,几个非常有用的算法可以实现为高阶函数,最基本的是map()filter()reduce()。大多数编程语言都附带提供这些函数版本的库,但以 DIY 方式,我们将研究可能的实现并检查细节。

In fact, it turns out that several very useful algorithms can be implemented as higher-order functions, the most fundamental ones being map(), filter(), and reduce(). Most programming languages ship with libraries that provide versions of these functions, but in DIY fashion, we’ll look at possible implementations and go over the details.

5.4.1.地图()

5.4.1. map()

背后的前提map()非常简单:给定某种类型的值集合,对每个值调用一个函数,然后返回结果集合。这种类型的处理在实践中反复出现,因此减少代码重复是有意义的。

The premise behind map() is very straightforward: given a collection of values of some type, call a function on each of those values, and return the collection of results. This type of processing shows up over and over in practice, so it makes sense to reduce code duplication.

我们以两个场景为例。首先,我们有一个数字数组,我们想要对数组中的每个数字进行平方。其次,我们有一个字符串数组,我们想要计算数组中每个字符串的长度。

Let’s take two scenarios as examples. First, we have an array of numbers, and we want to square each number in the array. Second, we have an array of strings, and we want to compute the length of each string in the array.

我们可以用几个for循环来实现这些示例,但是并排查看它们(如下一个清单所示)应该会给我们一种感觉,即一些共性可以抽象到共享代码中。

We could implement these examples with a couple of for loops, but looking at them side by side, as shown in the next listing, should give us a feeling that some of the commonality could be abstracted away into shared code.

清单 5.11。临时映射
让数字:数字[] = [1, 2, 3, 4, 5];                1个
让正方形:数字[] = [];

对于(常数 n 个数字){
    squares.push(n * n);                                2个
}

让字符串:string[] = ["apple", "orange", "peach"];   3个
让长度:数字[] = [];

for (const s of strings) {
    lengths.push(s.length);                             4 
}
let numbers: number[] = [1, 2, 3, 4, 5];                1
let squares: number[] = [];

for (const n of numbers) {
    squares.push(n * n);                                2
}

let strings: string[] = ["apple", "orange", "peach"];   3
let lengths: number[] = [];

for (const s of strings) {
    lengths.push(s.length);                             4
}

  • 1 数字数组
  • 1 Array of numbers
  • 2 对于数组中的每个数字,我们对其求平方并将其添加到正方形数组中。
  • 2 For each number in the array, we square it and add it to the squares array.
  • 3 字符串数组
  • 3 Array of strings
  • 4 对于数组中的每个字符串,我们将其长度添加到长度数组中。
  • 4 For each string in the array, we add its length to the lengths array.

虽然数组和转换不同,但结构显然非常相似(图 5.5)。

Although the arrays and transformations are different, the structure is obviously very similar (figure 5.5).

图 5.5。对数字求平方和获取字符串长度是截然不同的场景,但转换的整体结构是相同的:采用输入数组,应用函数,然后生成输出数组。

DIY地图

让我们看一下 for arrays 的实现map(),看看我们如何避免一遍又一遍地编写这种循环。我们将使用通用类型Tand U,因为无论Tand是什么,实现都有效U。这样,我们可以将此函数用于不同类型,而不是将其限制为数字数组。

Let’s look at an implementation of map() for arrays and see how we can avoid writing this kind of loop over and over. We’ll use generic types T and U, as the implementation works regardless of what T and U are. This way, we can use this function with different types, as opposed to restricting it to, say, arrays of numbers.

我们的函数接受一个 s 数组T和一个接受一个项目T作为参数并返回 type 值的函数U。它将结果收集到一个Us 数组中。下一个清单中的实现简单地遍历 s 数组中的每一项,将给定的函数应用于它,然后将结果存储在s T数组中。U

Our function takes an array of Ts and a function that takes an item T as argument and returns a value of type U. It collects the result in an array of Us. The implementation in the next listing simply goes over each item in the array of Ts, applies the given function to it, and then stores the result in the array of Us.

清单 5.12。map()
function map<T, U>(items: T[], func: (item: T) => U): U[] { 1     let 
    result: U[] = [];                                      2个

    对于(项目的常量项目){
        结果.推送(功能(项目));                               3个
    }

    返回结果;                                             4 
}
function map<T, U>(items: T[], func: (item: T) => U): U[] {    1
    let result: U[] = [];                                      2

    for (const item of items) {
        result.push(func(item));                               3
    }

    return result;                                             4
}

  • 1 map() 接受一个 T 类型的项目数组和一个从 T 到 U 的函数,并返回一个 Us 数组。
  • 1 map() takes an array of items of type T and a function from T to U, and returns an array of Us.
  • 2 从一个空的 Us 数组开始。
  • 2 Start with an empty array of Us.
  • 3 对于每个项目,将 func(item) 的结果推送到 Us 数组。
  • 3 For each item, push the result of func(item) to the array of Us.
  • 4 返回我们的数组。
  • 4 Return the array of Us.

这个简单的函数封装了前面例子的公共处理。使用map(),我们可以使用一对单行代码生成正方形数组和字符串长度数组,如以下清单所示。

This simple function encapsulates the common processing of the preceding example. With map(), we can produce the array of squares and the array of string lengths with a couple of one-liners, as the following listing shows.

清单 5.13。使用map()
让数字:数字[] = [1, 2, 3, 4, 5];
设正方形:number[] = map(numbers, (item) => item * item);    1个

让字符串:string[] = ["apple", "orange", "peach"];
让长度:number[] = map(strings, (item) => item.length);    2个
let numbers: number[] = [1, 2, 3, 4, 5];
let squares: number[] = map(numbers, (item) => item * item);    1

let strings: string[] = ["apple", "orange", "peach"];
let lengths: number[] = map(strings, (item) => item.length);    2

  • 1 使用 lambda (item) => item * item 调用 map()。(在这种情况下,项目是一个数字。)
  • 1 Call map() with the lambda (item) => item * item. (In this case, item is a number.)
  • 2 使用 lambda (item) => item.length 调用 map()。(在本例中,item 是一个字符串。)
  • 2 Call map() with the lambda (item) => item.length. (In this case, item is a string.)

map()封装我们作为参数给它的函数的应用程序。我们只需将一个项目数组和一个函数传递给它,然后我们取回应用该函数的结果数组。稍后,当我们讨论泛型时,我们将看到如何进一步推广它以使其适用于任何数据结构,而不仅仅是数组。但是,即使使用当前的实现,我们也可以获得一个非常好的抽象,可以将函数应用于项目集,我们可以在许多情况下重用它。

map() encapsulates the application of the function that we give it as argument. We just hand it an array of items and a function, and we get back the array resulting from the application of the function. Later, when we discuss generics, we’ll see how we can generalize this even further to make it work with any data structure, not only arrays. Even with the current implementation, though, we get a very good abstraction for applying functions to sets of items, which we can reuse in many situations.

5.4.2.筛选()

5.4.2. filter()

下一个非常常见的场景map()是的表亲filter()。给定项目集合和条件,过滤掉不满足条件的项目并返回满足条件的项目集合。

The next very common scenario, the cousin of map(), is filter(). Given a collection of items and a condition, filter out the items that don’t meet the condition and return the collection of items that do.

回到我们的数字和字符串示例,让我们过滤列表,以便我们只保留偶数和 length 的字符串5map()在这里不能帮助我们,因为它处理集合中的所有元素,但在这种情况下,我们想丢弃一些。临时实现将再次包括遍历集合并检查是否满足条件,如下一个清单所示。

Going back to our numbers and strings examples, let’s filter the list so that we keep only the even numbers and the strings of length 5. map() can’t help us here, as it processes all elements in the collection, but in this case, we want to discard some. The ad-hoc implementation would again consist of looping over the collections and checking whether the condition is met, as shown in the next listing.

清单 5.14。临时过滤
让数字:数字[] = [1, 2, 3, 4, 5];
让均匀:数字[] = []

对于(常数 n 个数字){
    如果 (n % 2 == 0) {                    1
        evens.push(n);
    }
}

让字符串:string[] = ["apple", "orange", "peach"];
让 length5Strings: string[] = [];

for (const s of strings) {
    如果(s.length == 5){                 2
        length5Strings.push(s);
    }
}
let numbers: number[] = [1, 2, 3, 4, 5];
let evens: number[] = []

for (const n of numbers) {
    if (n % 2 == 0) {                   1
        evens.push(n);
    }
}

let strings: string[] = ["apple", "orange", "peach"];
let length5Strings: string[] = [];

for (const s of strings) {
    if (s.length == 5) {                2
        length5Strings.push(s);
    }
}

  • 1 仅在偶数时才推送项目
  • 1 Push item only if it is even
  • 2 仅当长度为 5 时才推送项目
  • 2 Push item only if it has length 5

同样,我们立即看到了一个共同的底层结构(图 5.6)。

Again, we immediately see a common underlying structure (figure 5.6).

图 5.6。即使是具有长度的数字和字符串也5共享一个结构。我们遍历输入,应用过滤器,并输出过滤器返回的项目true

DIY过滤器

就像我们对 所做的那样map(),我们可以实现一个通用的filter()高阶函数,它将一个输入数组和一个过滤器函数作为参数,并返回过滤后的输出,如以下代码所示。在这种情况下,如果输入数组的类型为T,则过滤函数是一个以 aT作为参数并返回 a 的函数boolean。接受单个参数并返回 a 的函数boolean也称为谓词

Just as we did with map(), we can implement a generic filter() higher-order function that takes as arguments an input array and a filter function, and returns the filtered output, as shown in the following code. In this case, if the input array is of type T, the filter function is a function that takes a T as argument and returns a boolean. A function that takes a single argument and returns a boolean is also called a predicate.

清单 5.15。filter()
function filter<T>(items: T[], pred: (item: T) => boolean): T[] {     1
    让结果:T[] = [];

    对于(项目的常量项目){
        如果(预测(项目)){                                             2
            结果.推送(项目);
        }
    }

    返回结果;
}
function filter<T>(items: T[], pred: (item: T) => boolean): T[] {    1
    let result: T[] = [];

    for (const item of items) {
        if (pred(item)) {                                            2
            result.push(item);
        }
    }

    return result;
}

  • 1 filter() 采用 T 数组和谓词(从 T 到布尔值的函数)。
  • 1 filter() takes an array of Ts and a predicate (a function from T to boolean).
  • 2 如果谓词返回真,该项被添加到结果数组;否则,它被跳过。
  • 2 If the predicate returns true, the item is added to the result array; otherwise, it’s skipped.

让我们看看当我们使用我们在函数中实现的通用结构时过滤代码是什么样的filter()。偶数和长度的字符串都5成为下一个列表中的一行。

Let’s see what the filtering code looks like when we use the common structure that we implemented in our filter() function. Both the even numbers and strings of length 5 become one-liners in the next listing.

清单 5.16。使用filter()
让数字:数字[] = [1, 2, 3, 4, 5];
让 evens: number[] = filter(numbers, (item) => item % 2 == 0);

让字符串:string[] = ["apple", "orange", "peach"];
让 length5Strings: string[] = filter(strings, (item) => item.length == 5);
let numbers: number[] = [1, 2, 3, 4, 5];
let evens: number[] = filter(numbers, (item) => item % 2 == 0);

let strings: string[] = ["apple", "orange", "peach"];
let length5Strings: string[] = filter(strings, (item) => item.length == 5);

数组通过使用谓词进行过滤——在第一种情况下,true如果数字可以被 2 整除,则返回一个 lambda;在第二种情况下,true如果字符串具有长度,则返回一个 lambda 5

The arrays are filtered by using a predicate—in the first case, a lambda that returns true if the number is divisible by 2, and in the second case, a lambda that returns true if the string has length 5.

将第二个常用操作实现为泛型函数后,让我们继续讨论本章介绍的第三个也是最后一个操作。

With the second common operation implemented as a generic function, let’s move on to the third and last operation covered in this chapter.

5.4.3.减少()

5.4.3. reduce()

到目前为止,我们可以使用 将函数应用于项目集合map(),我们可以使用 将不符合特定条件的项目从集合中移除filter()。第三个常见操作涉及将所有集合项合并为一个值。

So far, we can apply a function to a collection of items by using map(), and we can remove items that don’t meet certain criteria from a collection by using filter(). The third common operation involves merging all the collection items into a single value.

例如,我们可能想要计算数字数组中所有数字的乘积,并将字符串数组中的所有字符串连接起来形成一个大字符串。这些场景不同,但具有共同的底层结构。首先,让我们看一下临时实施。

We might want to calculate the product of all numbers in a number array, for example, and concatenate all the strings in a string array to form one big string. These scenarios are different but have a common underlying structure. First, let’s look at the ad hoc implementation.

清单 5.17。特设还原
让数字:数字[] = [1, 2, 3, 4, 5];
让产品:数量= 1;                               1个

对于(常数 n 个数字){
    产品=产品* n;                             2个
}

让字符串:string[] = ["apple", "orange", "peach"];
让 longString: string = "";                           3个

for (const s of strings) {
    longString = longString + s;                       4 
}
let numbers: number[] = [1, 2, 3, 4, 5];
let product: number = 1;                               1

for (const n of numbers) {
    product = product * n;                             2
}

let strings: string[] = ["apple", "orange", "peach"];
let longString: string = "";                           3

for (const s of strings) {
    longString = longString + s;                       4
}

  • 1 在产品案例中,我们从初始值 1 开始。
  • 1 In the product case, we start with an initial value of 1.
  • 2 我们继续将产品乘以我们集合中的每个数字,累加结果。
  • 2 We proceed to multiply product by every number in our collection, accumulating the result.
  • 3 在字符串的情况下,我们从一个空字符串开始。
  • 3 In the string case, we start with an empty string.
  • 4 我们将每个字符串附加到空字符串,累加结果。
  • 4 We append each string to the empty string, accumulating the result.

在这两种情况下,我们都从一个初始值开始;然后我们通过遍历集合并将每个项目与累加器组合来累加结果。当我们遍历集合时,product包含数字数组中所有数字的乘积,并且longString是字符串数组中所有字符串的串联(图 5.7)。

In both scenarios, we start with an initial value; then we accumulate the result by going over the collections and combining each item with the accumulator. When we’re done going over the collections, product contains the product of all the numbers in the numbers array, and longString is the concatenation of all strings in the strings array (figure 5.7).

图 5.7。组合数字数组中的数字和字符串数组中的字符串的通用结构。在第一种情况下,初始值为1,我们应用的组合是与每个项目相乘。在第二种情况下,初始值为"",我们应用的组合是与每个项目的串联。

DIY减

清单 5.18中,我们将实现一个泛型函数,它接受一个 s 数组T、一个 type 的初始值T,以及一个接受两个 type 参数T并返回 a 的函数T。我们会将运行总计存储在一个局部变量中,并通过依次将函数应用于它和输入数组的每个元素来更新它。

In listing 5.18, we’ll implement a generic function that takes an array of Ts, an initial value of type T, and a function that takes two arguments of type T and returns a T. We’ll store the running total in a local variable and update it by applying the function to it and each element of the input array in turn.

清单 5.18。reduce()
function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T {     1
    让结果:T = init;

    对于(项目的常量项目){
        结果 = 操作(结果,项目);                                     2个
    }

    返回结果;
}
function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T {    1
    let result: T = init;

    for (const item of items) {
        result = op(result, item);                                     2
    }

    return result;
}

  • 1 reduce() 采用 T 数组、初始值和将两个 T 合二为一的操作。
  • 1 reduce() takes an array of Ts, an initial value, and an operation combining two Ts into one.
  • 2 通过使用给定的操作,数组中的每一项都与运行总计相结合。
  • 2 Each item in the array is combined with the running total by using the given operation.

这个函数有三个参数,其他函数有两个。我们需要一个初始值而不是从数组的第一个元素开始的原因是我们需要处理输入数组为空的情况。result如果集合中没有项目会怎样?有一个初始值涵盖了这种情况,因为我们会简单地返回它。

This function has three arguments, and the others have two. The reason why we need an initial value instead of starting with, say, the first element of the array is that we need to handle the case when the input array is empty. What would result be if there was no item in the collection? Having an initial value covers that case, as we would simply return that.

让我们看看如何更新我们的临时实现以使用reduce().

Let’s see how we can update our ad-hoc implementations to use reduce().

清单 5.19。使用reduce()
让数字:数字[] = [1, 2, 3, 4, 5];
让乘积:number = reduce(numbers, 1, (x, y) => x * y);        1个

让字符串:string[] = ["apple", "orange", "peach"];
让 longString: string = reduce(strings, "", (x, y) => x + y);    2个
let numbers: number[] = [1, 2, 3, 4, 5];
let product: number = reduce(numbers, 1, (x, y) => x * y);        1

let strings: string[] = ["apple", "orange", "peach"];
let longString: string = reduce(strings, "", (x, y) => x + y);    2

  • 1 对于数字,我们从初始值 1 和运算 (x, y) => x * y(乘法)开始。
  • 1 For numbers, we start with an initial value of 1 and the operation (x, y) => x * y (multiplication).
  • 2 对于字符串,我们从初始值""和操作 (x, y) => x + y(串联)开始。
  • 2 For strings, we start with an initial value of "" and the operation (x, y) => x + y (concatenation).

reduce()有一些其他两个函数所没有的微妙之处。除了需要初始值外,项目组合的顺序可能会影响最终结果。对于我们示例中的操作和初始值,情况并非如此。但是如果我们的初始字符串是 呢"banana"?然后,从左到右连接,我们会得到"bananaappleorangepeach".但是如果我们从右到左遍历数组,总是将项目添加到字符串的开头,我们会得到"appleorangepeachbanana"

reduce() has a few subtleties that the other two functions don’t. Besides requiring an initial value, the order in which the items are combined may affect the final result. For the operations and initial values in our example, that’s not the case. But what if our initial string was "banana"? Then, concatenating from left to right, we would get "bananaappleorangepeach". But if we traversed the array from right to left, always adding the item to the beginning of the string, we would get "appleorangepeachbanana".

或者,如果我们的组合操作将每个字符串的第一个字母附加在一起,将其应用于"apple"and "orange"first 将得到"ao". 再次应用到"ao""peach"会给我们"ap"。另一方面,如果我们从"orange"和开始"peach",我们就会有"op"。然后"apple"会给"op"我们"ao"图5.8)。

Or if our combining operation appended the first letters of each string together, applying that to "apple" and "orange" first would give us "ao". Applying it again to "ao" and "peach" would give us "ap". On the other hand, if we started with "orange" and "peach", we would have "op". Then "apple" and "op" would give us "ao" (figure 5.8).

图 5.8。将字符串数组与“两个字符串的第一个字母”操作相结合,在从左到右和从右到左应用时会给出不同的结果。在第一种情况下,我们从一个空字符串 and 开始"apple",然后是"a"and "orange",然后是"ao"and "peach",给我们"ap"。在第二种情况下,我们从"peach"and 开始一个空字符串,然后是"orange"and "p",给我们"op"; 然后"apple""op"给我们"ao"

按照惯例,reduce()是从左到右应用的,因此无论何时您遇到它作为库函数时,都应该安全地假设它是如何工作的。一些图书馆还提供从右到左的版本。Array例如,JavaScript类型同时具有reduce()reduceRight()方法。如果您想了解更多关于这背后的数学知识,请参阅边栏“Monoids”。

Conventionally, reduce() is applied left to right, so whenever you encounter it as a library function, it should be safe to assume that’s how it works. Some libraries also provide a right-to-left version. The JavaScript Array type, for example, has both reduce() and reduceRight() methods. See the sidebar “Monoids” if you want to learn more about the math behind this.

幺半群

在抽象代数中,我们处理集合和对这些集合的操作。正如我们之前看到的,我们可以将类型视为一组可能的值。T对接受两个Ts 并返回另一个T,的 type 的操作(T, T) => T可以解释为对值集的操作Tnumber和的集合+,例如(x, y) => x + y,形成一个代数结构。

In abstract algebra, we deal with sets and operations on those sets. As we saw previously, we can think of a type as a set of possible values. An operation on type T that takes two Ts and returns another T, (T, T) => T, can be interpreted as an operation on the set of values T. The set of number and +, which is (x, y) => x + y, for example, forms an algebraic structure.

这些结构由它们的操作属性定义。标识是操作所针对的元素换句话说,与任何其他元素组合会使其他元素保持不变。恒等式是集合为加法时,集合为乘法时为恒等式,集合为字符串拼接时为(空字符串)。 idTop(x, id) == op(id, x) == xid0number1number""string

These structures are defined by the properties of their operations. An identity is an element id of T for which the operation op(x, id) == op(id, x) == x. In other words, combining id with any other element leaves the other element unchanged. Identity is 0 when the set is number and the operation is addition, 1 when the set is number and the operation is multiplication, and "" (the empty string) when the set is string and the operation is string concatenation.

关联性是操作的一个属性,表示我们将它应用于元素序列的顺序无关紧要,因为我们最终会得到相同的结果。对于任何x, y, zof T, op(x, op(y, z)) == op(op(x, y), z)。例如,对于数字加法和乘法是正确的,但对于减法或我们的“两个字符串的第一个字母”操作则不是这样。

Associativity is a property of the operation that says the order in which we apply it to a sequence of elements doesn’t matter, as we’ll get the same result in the end. For any x, y, z of T, op(x, op(y, z)) == op(op(x, y), z). This is true, for example, for number addition and multiplication but not true for subtraction or our “first letter of both strings” operation.

如果T具有操作的集合op具有标识元素并且该操作是关联的,则生成的代数结构称为monoid。对于幺半群,以身份作为初始值,从左到右或从右到左减少会产生相同的结果。如果集合为空,我们甚至可以删除对初始值和默认标识的要求。我们也可以并行化归约。我们可以并行减少集合的前半部分和后半部分并组合结果,例如,因为关联性属性保证我们会得到相同的结果。对于[1, 2, 3, 4, 5, 6],我们可以并行组合1 + 2 + 34 + 5 + 6,然后将结果相加。

If a set T with an operation op has an identity element and the operation is associative, the resulting algebraic structure is called a monoid. For a monoid, starting with the identity as the initial value, reducing from left to right or right to left yields the same result. We can even remove the requirement for an initial value and default to the identity if the collection is empty. We can also parallelize reduction. We could reduce the first half and the second half of the collection in parallel and combine the results, for example, because the associativity property guarantees that we’ll get the same result. For [1, 2, 3, 4, 5, 6], we can combine 1 + 2 + 3 and, in parallel, 4 + 5 + 6, and then add the results together.

一旦我们放弃其中一个属性,我们就失去了这些保证。如果我们没有结合性,只有一个集合、一个操作和一个单位元素,虽然我们仍然不需要初始值(我们使用单位元素),但我们应用操作的方向变得很重要。如果我们删除标识元素但保持结合性,我们就有一个半群。没有身份,我们将初始值放在第一个元素的左侧还是最后一个元素的右侧很重要。

As soon as we drop one of the properties, we lose these guarantees. If we don’t have associativity, but just a set, an operation, and an identity element, although we still don’t require an initial value (we use the identity element), the direction in which we apply the operations becomes important. If we drop the identity element but keep associativity, we have a semigroup. Without an identity, it matters whether we put the initial value on the left of the first element or the right of the last element.

关键要点是reduce()在幺半群上无缝工作,但如果我们没有幺半群,我们应该小心我们使用什么作为我们的初始值和我们减少的方向。

The key takeaway is that reduce() works seamlessly on a monoid, but if we don’t have a monoid, we should be careful what we use for our initial value and the direction we’re reducing on.

5.4.4.库支持

5.4.4. Library support

如本节开头所述,大多数编程语言都具有对这些常用算法的库支持。但是,它们可能会以不同的名称出现,因为没有为它们命名的黄金标准。

As mentioned at the start of this section, most programming languages have library support for these common algorithms. They may show up under different names, though, as there is no golden standard for naming them.

在 C# 中,map()filter()reduce()在命名空间中分别显示System.LinqSelect()Where()Aggregate()。在 Java 中,它们显示为map()filter()reduce()in java.util.stream

In C#, map(), filter(), and reduce() show up in the System.Linq namespace as Select(), Where(), and Aggregate() respectively. In Java, they show up as map(), filter(), and reduce() in java.util.stream.

map()也称为Select()transform()filter()也被称为Where(). reduce()也称为accumulate()Aggregate()fold(),具体取决于语言和库。

map() is also known as Select() or transform(). filter() is also known as Where(). reduce() is also known as accumulate(), Aggregate(), or fold(), depending on the language and library.

尽管它们有很多名称,但这些算法在广泛的应用程序中都是基础和有用的。我们将在本书后面讨论许多类似的算法,但这三种算法构成了使用高阶函数进行数据处理的基础。

Even though they have many names, these algorithms are fundamental and useful across a broad range of applications. We’ll discuss many similar algorithms later in the book, but these three form the foundation of data processing using higher-order functions.

Google 著名的 MapReduce 大规模数据处理框架通过在多个节点上运行大规模并行操作并通过类操作组合结果,使用与map()和算法相同的基本原理。 reduce()map()reduce()

Google’s famous MapReduce large-scale data processing framework uses the same underlying principles of the map() and reduce() algorithms by running a massively parallel map() operation on multiple nodes and combining the results via a reduce()-like operation.

5.4.5.练习

5.4.5. Exercises

1个

实现一个first()接受 s 数组的函数和一个接受 a作为参数并返回 a 的T函数(用于谓词) 。将返回返回的数组的第一个元素,或者如果返回所有元素。 predTbooleanfirst()pred()trueundefinedpred()false

1

Implement a first() function that takes an array of Ts and a function pred (for predicate) that takes a T as an argument and returns a boolean. first() will return the first element of the array for which pred() returns true or undefined if pred() returns false for all elements.

2个

实现一个all()接受 s 数组的函数和一个接受 a作为参数并返回 a 的T函数(用于谓词) 。如果是数组的所有元素,将返回 true ;否则,它将返回。 predTbooleanall()pred()truefalse

2

Implement an all() function that takes an array of Ts and a function pred (for predicate) that takes a T as an argument and returns a boolean. all() will return true if pred() is true for all the elements of the array; otherwise, it will return false.

5.5. 函数式编程

5.5. Functional programming

尽管本章涵盖的材料有点复杂,但好消息是我们已经了解了函数式编程的大部分关键要素。如果您习惯于命令式、面向对象的语言,某些函数式语言的语法可能会令人反感。他们的类型系统通常提供和类型、乘积类型和一阶函数支持的一些组合,以及一组库函数,例如 、map()filter()reduce()处理数据。许多函数式语言使用惰性求值,我们也在本章中讨论过。

Although the material covered in this chapter was a bit more complex, the good news is that we went over most of the key ingredients of functional programming. The syntax of some functional languages may be off-putting if you’re used to imperative, object-oriented languages. Their type systems usually offer some combinations of sum types, product types, and first-order function support, as well as a set of library functions such as map(), filter(), and reduce() to process data. Many functional languages employ lazy evaluation, which we also discussed in this chapter.

有了对函数进行类型化的能力,许多源自函数式编程语言的概念可以用非函数式(或纯函数式)的语言来实现。我们在本章中看到了这一点;我们触及了所有这些主题,并为所有这些关键组件提供了必要的实现。

With the ability to type functions, many of the concepts originating from functional programming languages can be implemented in languages that aren’t functional (or purely functional). We saw this throughout this chapter; we touched on all these topics and provided imperative implementations for all these key components.

概括

Summary

  • 如果我们可以键入函数,我们就可以通过关注实现逻辑的函数并丢弃周围的脚手架,以更简单的方式实现策略模式。
  • If we can type functions, we can implement the strategy pattern in a much simpler way by focusing on the functions that implement the logic and discarding the surrounding scaffolding.
  • 将函数作为属性插入类并将其作为方法调用的能力使我们能够实现不依赖大switch语句的状态机。这样,编译器可以防止错误,例如在某些给定状态下意外应用错误的处理。
  • The ability to plug a function into a class as a property and call it as a method allows us to implement state machines that don’t rely on big switch statements. This way, the compiler can prevent mistakes like accidentally applying the wrong processing in some given state.
  • 状态机实现语句的另一种替代方法switch是求和类型,其中每个状态都由不同的类型捕获。
  • Another alternative to switch statements for a state machine implementation is a sum type in which each state is captured by a different type.
  • 我们可以通过依赖惰性值来推迟昂贵的计算,惰性值是我们传递的包装昂贵计算的函数。我们在需要产生值时调用它们,但如果我们从不需要它们,我们可以跳过昂贵的计算。
  • We can defer expensive computation by relying on lazy values, which are functions we pass around that wrap the expensive computation. We call them when needed to produce a value, but if we never need them, we can skip the expensive computation.
  • Lambda 是无名函数,我们可以将其用于一次性逻辑,在这种逻辑中,命名函数不会很有用。
  • Lambdas are nameless functions we can use for one-off logic in which naming a function wouldn’t be very useful.
  • 高阶函数是将另一个函数作为参数或返回函数的函数。
  • A higher order function is a function that takes another function as an argument or returns a function.
  • map(), filter(), 和reduce()是三个基本的高阶函数,在数据处理中有很多应用。
  • map(), filter(), and reduce() are three fundamental higher-order functions, with many applications in data processing.

第 6 章中,我们将研究类型化函数的更多应用。我们将了解闭包以及如何使用它们来简化另一种常见的设计模式:装饰器模式。我们还将讨论承诺以及任务分配和事件驱动系统。所有这些应用程序都可以通过将计算(函数)表示为类型系统的一等公民的能力来实现。

In chapter 6, we’ll look at a few more applications of typed functions. We’ll learn about closures and how we can use them to simplify another common design pattern: the decorator pattern. We’ll also talk about promises, as well as tasking and event-driven systems. All these applications are made possible by the ability to represent computation (functions) as first-class citizens of the type system.

习题答案

Answers to exercises

一个简单的策略模式

A simple strategy pattern

1个

b—那是唯一的函数类型;其他声明不代表功能。

1

b—That is the only function type; the other declarations do not represent functions.

2个

c—该函数接受 anumber和 an(x: number) => boolean并返回boolean

2

c—The function takes a number and an (x: number) => boolean and returns boolean.

 

 

没有 switch 语句的状态机

A state machine without switch statements

1个

我们可以将连接建模为具有两个状态的状态机——openclosed——以及两个转换——从到的connect转换和从到的断开转换。 closedopenopenclosed

1

We can model the connection as a state machine with two states—open and closed—and two transitions—connect transitions from closed to open and disconnect transitions from open to closed.

2个

一个可能的实现:

声明函数 read(): string;

类连接{
    私有 doProcess: () => void = this.processClosedConnection;

    公共过程():无效{
        这个.doProcess();
    }

    私有 processClosedConnection() {
        this.doProcess = this.processOpenConnection;
    }

    私人进程OpenConnection(){
        常量值:string = read();

        如果(值。长度== 0){
            this.doProcess = this.processClosedConnection;
        } 别的 {
            控制台日志(值);
        }
    }
}

2

A possible implementation:

declare function read(): string;

class Connection {
    private doProcess: () => void = this.processClosedConnection;

    public process(): void {
        this.doProcess();
    }

    private processClosedConnection() {
        this.doProcess = this.processOpenConnection;
    }

    private processOpenConnection() {
        const value: string = read();

        if (value.length == 0) {
            this.doProcess = this.processClosedConnection;
        } else {
            console.log(value);
        }
    }
}

 

 

避免使用惰性值进行昂贵的计算

Avoiding expensive computation with lazy values

1个

d——其他实现命名函数;这是唯一的匿名实现。

1

d—The other implement named functions; this is the only anonymous implementation.

 

 

使用 map、filter 和 reduce

Using map, filter, and reduce

1个

一个可能的实现first()

函数 first<T>(items: T[], pred: (item: T) => boolean):
    吨 | 不明确的 {
    对于(项目的常量项目){
        如果(预测(项目)){
            归还物品;
        }
    }

    返回未定义;
}

1

A possible implementation for first():

function first<T>(items: T[], pred: (item: T) => boolean):
    T | undefined {
    for (const item of items) {
        if (pred(item)) {
            return item;
        }
    }

    return undefined;
}

2个

一个可能的实现all()

函数 all<T>(items: T[], pred: (item: T) => boolean): boolean {
    对于(项目的常量项目){
        如果(!pred(项目)){
            返回假;
        }
    }

    返回真;
}

2

A possible implementation for all():

function all<T>(items: T[], pred: (item: T) => boolean): boolean {
    for (const item of items) {
        if (!pred(item)) {
            return false;
        }
    }

    return true;
}

 

 

第 6 章。函数类型的高级应用

Chapter 6. Advanced applications of function types

本章涵盖

This chapter covers

  • 使用简化的装饰器模式
  • Using a simplified decorator pattern
  • 实现可恢复计数器
  • Implementing a resumable counter
  • 处理长时间运行的操作
  • Handling long-running operations
  • 使用 promises 和编写干净的异步代码async/await
  • Writing clean asynchronous code by using promises and async/await

第 5 章中,我们介绍了函数类型的基础知识以及通过将函数作为参数传递并将它们作为结果返回而像对待其他值一样对待函数的能力所支持的场景。我们还研究了一些实现常见数据处理模式的强大抽象:map()filter()reduce()

In chapter 5, we covered the basics of function types and scenarios enabled by the ability to treat functions like other values by passing them as arguments and returning them as results. We also looked at some powerful abstractions that implement common data processing patterns: map(), filter(), and reduce().

在本章中,我们将通过一些更高级的应用程序继续讨论函数类型。我们将从查看装饰器模式、它的书本实现和替代实现开始。(再次强调,如果您忘记了它,请不要担心;我们会快速复习一下。)我们将介绍闭包的概念看看我们如何使用它来实现一个简单的计数器。然后我们将看看另一种实现计数器的方法,这次使用生成器:一个产生多个结果的函数。

In this chapter, we’ll continue our discussion of function types with some more advanced applications. We’ll start by looking at the decorator pattern, its by-the-book implementation, and an alternative implementation. (Again, don’t worry if you forgot it; we’ll have a quick refresher.) We’ll introduce the concept of a closure and see how we can use it to implement a simple counter. Then we’ll look at another way to implement a counter, this time with a generator: a function that yields multiple results.

接下来,我们将讨论异步操作。我们将回顾两个主要的异步执行模型——线程和事件循环——并看看我们如何对几个长时间运行的操作进行排序。我们将从回调开始;然后我们将看看承诺,最后,我们将介绍当今大多数主流编程语言提供的async/await语法。

Next, we’ll talk about asynchronous operations. We’ll go over the two main asynchronous execution models—threads and event loops—and look at how we can sequence several long-running operations. We’ll start with callbacks; then we’ll look at promises, and finally, we’ll cover the async/await syntax provided nowadays by most mainstream programming languages.

本章讨论的所有主题之所以成为可能,是因为我们可以将函数用作值,正如我们将在接下来的几页中看到的那样。

All the topics discussed in this chapter are made possible because we can use functions as values, as we’ll see in the following pages.

6.1. 一个简单的装饰器模式

6.1. A simple decorator pattern

装饰者模式是一种行为软件设计模式,它在不修改对象的类的情况下扩展对象的行为。装饰对象可以执行超出其原始实现所提供的工作。该模式如图6.1所示。

The decorator pattern is a behavioral software design pattern that extends the behavior of an object without modifying the class of the object. A decorated object can perform work beyond what its original implementation provides. The pattern looks like figure 6.1.

图 6.1。装饰器模式:一个IComponent接口,一个通过 的具体实现ConcreteComponent,以及一个通过附加行为 Decorator增强的IComponent

作为一个例子,假设我们有一个IWidgetFactory声明一个make-Widget()方法返回一个的Widget。具体实现,Widget-Factory,实现实例化新对象的方法Widget

As an example, suppose that we have an IWidgetFactory that declares a make-Widget() method returning a Widget. The concrete implementation, Widget-Factory, implements the method to instantiate new Widget objects.

假设我们想要重用 a Widget,那么我们不想总是创建一个新的,而是只想创建一个并不断返回它(也就是说,有一个单例)。没有 修改我们的WidgetFactory,我们可以创建一个名为 的装饰器Singleton-Decorator,它包装一个IWidgetFactory,如下一个清单所示,并扩展其行为以确保只Widget创建一个(图 6.2)。

Suppose that we want to reuse a Widget, so instead of always creating a new one, we want to create just one and keep returning it (that is, have a singleton). Without modifying our WidgetFactory, we can create a decorator called Singleton-Decorator, which wraps an IWidgetFactory, as shown in the next listing, and extends its behavior to ensure that only a single Widget gets created (figure 6.2).

图 6.2。小部件工厂的装饰器模式。IWidgetFactory是接口,WidgetFactory是一个具体的实现,并将SingletonDecorator单例行为添加到IWidgetFactory.

清单 6.1。WidgetFactory装潢师
类小部件 { }

接口 IWidgetFactory {
    makeWidget(): 小部件;
}

类 WidgetFactory 实现 IWidgetFactory {
    公共 makeWidget(): 小部件 {
        返回新的小部件();                            1个
    }
}

类 SingletonDecorator 实现 IWidgetFactory {
    私有工厂:IWidgetFactory;                    2个
    私有实例:小部件 | 未定义=未定义;

    构造函数(工厂:IWidgetFactory){
        这个.工厂=工厂;
    }

    公共 makeWidget(): 小部件 {
        if (this.instance == undefined) {                3 
            this.instance = this.factory.makeWidget();  3个
        }

        返回这个实例;
    }
}
class Widget { }

interface IWidgetFactory {
    makeWidget(): Widget;
}

class WidgetFactory implements IWidgetFactory {
    public makeWidget(): Widget {
        return new Widget();                            1
    }
}

class SingletonDecorator implements IWidgetFactory {
    private factory: IWidgetFactory;                    2
    private instance: Widget | undefined = undefined;

    constructor(factory: IWidgetFactory) {
        this.factory = factory;
    }

    public makeWidget(): Widget {
        if (this.instance == undefined) {               3
            this.instance = this.factory.makeWidget();  3
        }

        return this.instance;
    }
}

  • 1 WidgetFactory 只是创建一个新的Widget。
  • 1 WidgetFactory simply creates a new Widget.
  • 2 SingletonDecorator 包装一个 IWidgetFactory。
  • 2 SingletonDecorator wraps an IWidgetFactory.
  • 3 makeWidget() 实现单例逻辑,保证只创建一个Widget。
  • 3 makeWidget() implements the singleton logic and ensures that only a single Widget is created.

使用这种模式的好处是它支持单一职责原则,即一个类应该只有一个职责。在这种情况下,Widget-Factory负责创建小部件,而SingletonDecorator负责单例行为。如果我们想要多个实例,我们直接使用Widget-Factory。如果我们想要单个实例,我们使用SingletonDecorator.

The advantage of using this pattern is that it supports the single-responsibility principle, which says that a class should have just one responsibility. In this case, the Widget-Factory is responsible for creating widgets, whereas the SingletonDecorator is responsible for the singleton behavior. If we want multiple instances, we use the Widget-Factory directly. If we want a single instance, we use SingletonDecorator.

6.1.1.功能性装饰器

6.1.1. A functional decorator

让我们看看如何再次使用类型化函数来简化此实现。首先,让我们摆脱IWidgetFactory接口并用函数类型替换它。Widget那将是不带参数并返回:的函数类型 () => Widget

Let’s see how we can simplify this implementation, again by using typed functions. First, let’s get rid of the IWidgetFactory interface and replace it with a function type. That would be the type of a function that takes no arguments and returns a Widget: () => Widget.

现在我们可以WidgetFactory用一个简单的函数替换我们的类,make-Widget(). 每当我们使用IWidgetFactory之前,传入一个实例时WidgetFactory,我们现在需要一个类型的函数() => Widget并传入makeWidget(),如以下清单所示。

Now we can replace our WidgetFactory class with a simple function, make-Widget(). Whenever we would’ve used an IWidgetFactory before, passing in an instance of WidgetFactory, we now require a function of type () => Widget and pass in makeWidget(), as the following listing shows.

清单 6.2。功能部件工厂
类小部件 { }

输入 WidgetFactory = () => 小部件;                 1个

函数 makeWidget(): 小部件 {                     2
    返回新的小部件();
}

函数 use10Widgets(工厂:WidgetFactory){     3
    对于(设 i = 0;i < 10;i++){
        让小部件=工厂();
        /* ... */
    }
}

使用 10Widgets(makeWidget);                          4个
class Widget { }

type WidgetFactory = () => Widget;                 1

function makeWidget(): Widget {                    2
    return new Widget();
}

function use10Widgets(factory: WidgetFactory) {    3
    for (let i = 0; i < 10; i++) {
        let widget = factory();
        /* ... */
    }
}

use10Widgets(makeWidget);                          4

  • 1 小部件工厂的函数类型
  • 1 Function type for a widget factory
  • 2 makeWidget() 是 WidgetFactory 类型。
  • 2 makeWidget() is of type WidgetFactory.
  • 3 use10Widgets() 需要一个 WidgetFactory,它用于创建 10 个 Widget 实例。
  • 3 use10Widgets() requires a WidgetFactory, which it uses to create 10 Widget instances.
  • 4 示例调用:我们将 makeWidget 函数作为参数传递。
  • 4 Example call: we pass the makeWidget function as an argument.

对于功能部件工厂,我们使用了一种与第 5 章中的策略模式非常相似的技术:我们获取一个函数作为参数,并在需要时调用它。现在让我们看看如何添加单例行为。

With the functional widget factory, we use a technique very similar to the strategy pattern in chapter 5: we get a function as an argument and call it when needed. Now let’s see how we can add the singleton behavior.

我们提供了一个新函数 ,singletonDecorator()它接受一个Widget-Factory-type 函数并返回另一个WidgetFactory-type 函数。请记住第 5 章,lambda 是一个没有名字的函数,我们可以从另一个函数返回它。在下一个清单中,我们的装饰器将使用一个工厂并使用它来构建一个处理单例行为的新函数(图 6.3)。

We provide a new function, singletonDecorator(), that takes a Widget-Factory-type function and returns another WidgetFactory-type function. Remember from chapter 5 that a lambda is a function without a name, which we can return from another function. In the next listing, our decorator will take a factory and use it to build a new function that handles the singleton behavior (figure 6.3).

图 6.3。函数式装饰器:我们现在只有一个makeWidget()函数和一个singletonDecorator()函数。

清单 6.3。功能部件工厂装饰器
类小部件 { }

输入 WidgetFactory = () => 小部件;

函数 makeWidget(): 小部件 {
    返回新的小部件();
}

function singletonDecorator(factory: WidgetFactory): WidgetFactory {
    让实例: Widget | 未定义=未定义;

    return (): Widget => {                       1
         if (instance == undefined) { 
            instance = factory(); 
        }

        返回实例;
    }; 
}

函数 use10Widgets(工厂:WidgetFactory){
    对于(设 i = 0;i < 10;i++){
        让小部件=工厂();
        /* ... */
    }
}

使用 10Widgets( singletonDecorator(makeWidget) );    2个
class Widget { }

type WidgetFactory = () => Widget;

function makeWidget(): Widget {
    return new Widget();
}

function singletonDecorator(factory: WidgetFactory): WidgetFactory {
    let instance: Widget | undefined = undefined;

    return (): Widget => {                      1
        if (instance == undefined) {
            instance = factory();
        }

        return instance;
    };
}

function use10Widgets(factory: WidgetFactory) {
    for (let i = 0; i < 10; i++) {
        let widget = factory();
        /* ... */
    }
}

use10Widgets(singletonDecorator(makeWidget));    2

  • 1 singletonDecorator() 返回一个实现单例行为的 lambda,并使用给定的工厂创建一个 Widget。
  • 1 singletonDecorator() returns a lambda that implements the singleton behavior and uses the given factory to create a Widget.
  • 2 因为 singletonDecorator() 返回一个 WidgetFactory,我们可以将它作为参数传递给 use10Widgets()。
  • 2 Because singletonDecorator() returns a WidgetFactory, we can pass it as an argument to use10Widgets().

现在,将调用 lambda ,而不是构造 10 个Widget对象,它将为所有调用重用相同的实例。 use10Widgets()Widget

Now, instead of constructing 10 Widget objects, use10Widgets() will call the lambda, which will reuse the same Widget instance for all calls.

这段代码减少了组件的数量,从一个接口和两个类,每个类都有一个方法(具体操作和装饰器)到两个函数。

This code reduces the number of components from an interface and two classes, each with a method (the concrete operation and the decorator) to two functions.

6.1.2.装饰器实现

6.1.2. Decorator implementations

与我们的策略模式一样,面向对象和函数式方法实现了相同的装饰器模式。面向对象的版本需要一个接口声明 ( IWidgetFactory),该接口的至少一个实现 ( Widget-Factory),以及一个处理添加的行为的装饰器类 ( Singleton-Decorator)。相比之下,函数式实现仅声明工厂函数 () 的类型() => Widget并使用两个函数:工厂函数 ( makeWidget()) 和装饰器函数 ( singletonDecorator())。

As with our strategy pattern, the object-oriented and functional approaches implement the same decorator pattern. The object-oriented version requires an interface declaration (IWidgetFactory), at least one implementation of that interface (Widget-Factory), and a decorator class that handles the added behavior (Singleton-Decorator). By contrast, the functional implementation simply declares the type of the factory function (() => Widget) and uses two functions: a factory function (makeWidget()) and a decorator function (singletonDecorator()).

需要注意的一件事是,在功能情况下,装饰器的类型与makeWidget(). 工厂不期望任何参数并返回 a Widget,而装饰器采用一个小部件工厂并返回另一个小部件工厂。换句话说,singletonDecorator()接受一个函数作为参数并返回一个函数作为它的结果。如果没有一流的函数,这是不可能的:将函数视为任何其他变量并将它们用作参数和返回值的能力。

One thing to note is that in the functional case, the decorator does not have the same type as makeWidget(). Whereas the factory doesn’t expect any arguments and returns a Widget, the decorator takes a widget factory and returns another widget factory. In other words, singletonDecorator() takes a function as an argument and returns a function as its result. This wouldn’t be possible without first-class functions: the ability to treat functions as any other variables and use them as arguments and return values.

现代类型系统支持的更简洁的实现适用于许多情况。当我们处理多个函数时,我们可以使用更详细的面向对象解决方案。如果我们的接口声明了几个方法,我们就不能用一个单一的函数类型来替换它。

The more-succinct implementation, enabled by modern type systems, is good for many situations. We can use the more-verbose object-oriented solution when we are dealing with more than a single function. If our interface declares several methods, we can’t replace it with a single function type.

6.1.3.闭包

6.1.3. Closures

让我们放大清单 6.4singletonDecorator()中的实现。您可能已经注意到了一些有趣的事情:尽管函数返回一个 lambda,但 lambda 引用了 argument和 variable ,它们应该是函数的本地变量。 factoryinstancesingletonDecorator()

Let’s zoom in on the singletonDecorator() implementation in listing 6.4. You may have noticed something interesting: even though the function returns a lambda, the lambda references both the factory argument and the variable instance, which should be local to the singletonDecorator() function.

清单 6.4。装饰函数
函数 singletonDecorator(工厂:WidgetFactory):WidgetFactory {
    让实例:小部件 | 未定义=未定义;

    返回():小部件=> {
        如果(实例==未定义){
            实例=工厂();
        }

        返回实例;
    };
}
function singletonDecorator(factory: WidgetFactory): WidgetFactory {
    let instance: Widget | undefined = undefined;

    return (): Widget => {
        if (instance == undefined) {
            instance = factory();
        }

        return instance;
    };
}

即使在我们从 返回之后singletonDecorator(),该instance变量仍然存在,因为它被 lambda“捕获”了,这被称为lambda 捕获

Even after we return from singletonDecorator(), the instance variable is still alive, as it was “captured” by the lambda, which is known as a lambda capture.

闭包和 lambda 捕获

lambda 捕获是在 lambda 中捕获的外部变量。编程语言通过闭包实现 lambda 捕获。闭不仅仅是一个简单的函数:它还记录了创建函数的环境,因此它可以在调用之间保持状态。

A lambda capture is an external variable captured within a lambda. Programming languages implement lambda captures through closures. A closure is something more than a simple function: it also records the environment in which the function was created, so it can maintain state between calls.

在我们的例子中,instance变量 insingletonDecorator()是该环境的一部分。我们返回的 lambda 仍然可以引用instance图 6.4)。

In our case, the instance variable in singletonDecorator() is part of that environment. The lambda we return will still be able to reference instance (figure 6.4).

图 6.4。一个返回闭包的简单函数:一个引用函数局部变量的 lambda。即使在getClosure()返回之后,这个变量仍然被闭包引用,所以它比它所在的函数还存在。

只有当我们有高阶函数时,闭包才有意义。如果我们不能从另一个函数返回一个函数,就没有环境可以捕获。在那种情况下,所有功能都在全局范围内,这是它们的环境。他们可以引用全局变量。

Closures make sense only if we have higher-order functions. If we can’t return a function from another function, there is no environment to capture. In that case, all functions are in the global scope, which is their environment. They can reference global variables.

另一种思考闭包的方法是将它们与对象进行对比。对象用一组方法表示某种状态;闭包表示具有某些捕获状态的函数。让我们看另一个可以使用闭包的例子:实现一个计数器。

Another way to think about closures is to contrast them with objects. An object represents some state with a set of methods; a closure represents a function with some captured state. Let’s look at another example in which closures can be used: implementing a counter.

6.1.4.练习

6.1.4. Exercises

1个

实现一个函数 ,loggingDecorator()该函数将另一个函数 作为参数,factory()该函数不接受任何参数并返回一个Widget对象。装饰给定的函数,以便无论何时调用它,它都会"Widget created"在返回Widget对象之前进行记录。

1

Implement a function, loggingDecorator(), that takes as argument another function, factory(), that takes no arguments and returns a Widget object. Decorate the given function so that whenever it is called, it logs "Widget created" before returning a Widget object.

6.2. 实现一个计数器

6.2. Implementing a counter

让我们来看一个非常简单的场景:我们想要创建一个计数器,它为我们提供从 1 开始的连续数字。尽管这个示例可能看起来微不足道,但它涵盖了几种可能的实现,这些实现可以推广到我们需要生成值的任何场景。一种方法是使用一个全局变量和一个返回该变量然后递增的函数,如以下代码所示。

Let’s look at a very simple scenario: we want to create a counter that gives us consecutive numbers starting from 1. Although this example may seem trivial, it covers several possible implementations that generalize to any scenario in which we need to generate values. One way is to use a global variable and a function that returns that variable and then increments, as shown in the following code.

清单 6.5。全球柜台
让 n: 数字 = 1;      1个

功能下一个(){
    返回 n++;         2个
}

控制台日志(下一个());    3
控制台日志(下一步());    3
控制台日志(下一步());    3个
let n: number = 1;      1

function next() {
    return n++;         2
}

console.log(next());    3
console.log(next());    3
console.log(next());    3

  • 1 计数器存储在全局变量中。
  • 1 The counter is stored in a global variable.
  • 2 next() 返回 n 并递增。
  • 2 next() returns n and increments.
  • 3 这将记录 1 2 3。
  • 3 This will log 1 2 3.

此实现有效,但并不理想。首先,n是一个全局变量,因此任何人都可以访问它。其他代码可能会从我们下面改变它。其次,这个实现给了我们一个单一的计数器。如果我们想要两个计数器,都从 1 开始怎么办?

This implementation works, but it’s not ideal. First, n is a global variable, so anyone has access to it. Other code might change it from underneath us. Second, this implementation gives us a single counter. What if we want two counters, both starting from 1?

6.2.1.面向对象的计数器

6.2.1. An object-oriented counter

我们要看的第一个实现是面向对象的,应该很熟悉。我们创建一个Counter类,它将计数器的状态存储为私有成员。我们提供了一种next()方法,它返回并递增该计数器。通过这种方式,我们封装了计数器,使外部代码无法更改它,并且我们可以创建任意数量的计数器作为此类的实例。

The first implementation we will look at is an object-oriented one, which should be familiar. We create a Counter class, which stores the state of the counter as a private member. We provide a next() method, which returns and increments that counter. In this way, we encapsulate the counter so that external code can’t change it and we can create as many counters as we want as instances of this class.

清单 6.6。面向对象计数器
类计数器 {
    私人 n: 数字 = 1;               1个

    下一个():数字{
        返回这个.n++;
    }
}

让 counter1: Counter = new Counter();   2
让counter2:Counter = new Counter();   2个

console.log(counter1.next());            3 
console.log(counter2.next());            3 
console.log(counter1.next());            3 
console.log(counter2.next());            3个
class Counter {
    private n: number = 1;               1

    next(): number {
        return this.n++;
    }
}

let counter1: Counter = new Counter();   2
let counter2: Counter = new Counter();   2

console.log(counter1.next());            3
console.log(counter2.next());            3
console.log(counter1.next());            3
console.log(counter2.next());            3

  • 1 计数器值现在是类私有的。
  • 1 The counter value is now private to the class.
  • 2 我们可以创建多个计数器。
  • 2 We can create multiple counters.
  • 3 这将记录 1 1 2 2。
  • 3 This will log 1 1 2 2.

这种方法效果更好。事实上,大多数现代编程语言都为像我们的计数器这样的类型提供了一个接口,它在每次调用时都提供一个值,并有特殊的语法来迭代它。在 TypeScript 中,这是通过Iterable接口和for ... of循环完成的。我们将在本书后面讨论泛型编程时讨论这个主题。现在,我们只注意到这种模式很常见。C# 通过IEnumerable接口和foreach循环实现它,而 Java 通过Iterable接口和for : loop.

This approach works better. In fact, most modern programming languages provide an interface for types such as our counter, which provides a value on each call and has special syntax to iterate over it. In TypeScript, this is done with the Iterable interface and for ... of loop. We cover this topic later in the book, when we discuss generic programming. For now, we’ll just note that this pattern is common. C# implements it with the IEnumerable interface and the foreach loop, whereas Java does it with the Iterable interface and the for : loop.

接下来,让我们看一下利用闭包来实现计数器的功能替代方案。

Next, let’s look at a functional alternative that leverages closures to implement the counter.

6.2.2.功能计数器

6.2.2. A functional counter

makeCounter()在下一个清单中,我们将通过一个在调用时返回计数器函数的函数来实现功能计数器。我们将计数器初始化为局部变量makeCounter(),然后在返回函数中捕获它。

In the next listing, we’ll implement the functional counter through a makeCounter() function that returns a counter function when called. We will initialize the counter as a variable local to makeCounter() and then capture it in the return function.

清单 6.7。功能计数器
输入 Counter = () => 数字;         1个

函数 makeCounter(): 计数器 {
    让 n: 数字 = 1;               2 
                                     2
    返回 () => n++;                2个
}

让 counter1: Counter = makeCounter();
让 counter2: Counter = makeCounter();

console.log(counter1());             3 
console.log(counter2());             3 
console.log(counter1());             3 
console.log(counter2());             3个
type Counter = () => number;         1

function makeCounter(): Counter {
    let n: number = 1;               2
                                     2
    return () => n++;                2
}

let counter1: Counter = makeCounter();
let counter2: Counter = makeCounter();

console.log(counter1());             3
console.log(counter2());             3
console.log(counter1());             3
console.log(counter2());             3

  • 1 我们将 Counter 类型定义为不带参数并返回数字的函数。
  • 1 We define a Counter type as a function that takes no arguments and returns a number.
  • 2 计数器值声明为变量并由 lambda 捕获。
  • 2 The counter value is declared as a variable and captured by the lambda.
  • 3 这将记录 1 1 2 2。
  • 3 This will log 1 1 2 2.

counter1.next()现在每个计数器都是一个函数,所以我们不调用,而是简单地调用counter1()。我们还看到每个计数器捕获一个单独的值:调用counter1()不会影响counter2(),因为每当我们调用时makeCounter(),都会创建一个新值n。每个返回的函数都有自己的n. 柜台是关闭的。此外,这些值在调用之间持续存在。这种行为与函数局部变量的行为不同,函数局部变量在调用函数时创建,在函数返回时销毁(图 6.5)。

Each counter is a function now, so instead of calling counter1.next(), we simply call counter1(). We also see that each counter captures a separate value: calling counter1() does not affect counter2() because whenever we call makeCounter(), a new n gets created. Each function returned keeps its own n. The counters are closures. Also, these values persist between calls. This behavior is different from that of variables that are local to a function, which are created when the function is called and disposed of when the function returns (figure 6.5).

图 6.5。重要的是要了解每个闭包(在我们的例子中是counter1and counter2)以不同的n. 每当我们调用 时makeCounter(),都会将 newn初始化为 1 并由返回的闭包捕获。因为这些值是分开的,所以它们不会相互干扰。

6.2.3.可恢复计数器

6.2.3. A resumable counter

定义计数器的另一种方法是使用可恢复函数。面向对象的计数器通过私有成员跟踪状态。功能计数器在其捕获的上下文中跟踪状态。

Another way to define a counter is to use a resumable function. An object-oriented counter keeps track of state via a private member. A functional counter keeps track of state in its captured context.

可恢复函数

可恢复函数是一种跟踪其自身状态的函数,无论何时被调用,都不会从头开始运行;相反,它会从上次返回时中断的地方继续执行。

A resumable function is a function that keeps track of its own state and, whenever it gets called, doesn’t run from the beginning; rather, it resumes executing from where it left off the last time it returned.

在 TypeScript 中,我们不使用关键字return来退出函数,而是使用关键字,如清单 6.8yield所示。此关键字暂停函数,将控制权交还给调用者。再次调用时,将从语句恢复执行 。 yield

In TypeScript, instead of using the keyword return to exit the function, we use the keyword yield, as shown in listing 6.8. This keyword suspends the function, giving control back to the caller. When called again, execution is resumed from the yield statement.

using 有更多限制yield:函数必须声明为生成器,并且它的返回类型必须是可迭代的迭代器。生成器通过在函数名称前加上星号来声明。

There are a couple more constraints for using yield: the function must be declared as a generator, and its return type must be an iterable iterator. A generator is declared by prefixing the function name with an asterisk.

清单 6.8。可恢复计数器
函数* counter(): IterableIterator<number> {        1
    让 n: 数字 = 1;

    而(真){
        产量n++;                                    2个
    }
}

让 counter1: IterableIterator<number> = counter();   3
让 counter2: IterableIterator<number> = counter();   3个

console.log(counter1.next());                         4 
console.log(counter2.next());                         4 
console.log(counter1.next());                         4 
console.log(counter2.next());                         4个
function* counter(): IterableIterator<number> {       1
    let n: number = 1;

    while (true) {
        yield n++;                                    2
    }
}

let counter1: IterableIterator<number> = counter();   3
let counter2: IterableIterator<number> = counter();   3

console.log(counter1.next());                         4
console.log(counter2.next());                         4
console.log(counter1.next());                         4
console.log(counter2.next());                         4

  • 1 该函数被声明为生成器。
  • 1 The function is declared as a generator.
  • 2 我们调用 yield 而不是 return。
  • 2 We call yield instead of return.
  • 3 我们的计数器是实现 IterableIterator 接口的对象。
  • 3 Our counters are objects implementing the IterableIterator interface.
  • 4 这记录了 1 1 2 2。
  • 4 This logs 1 1 2 2.

这种实现在某种程度上是我们的面向对象和功能计数器之间的混合。计数器的实现读起来像一个函数:我们从n1 开始,然后永远循环,产生计数器值并递增它。另一方面,编译器生成的代码是面向对象的:我们的计数器实际上是一个IterableIterator<number>,我们调用next()它来获取下一个值。

This implementation is in a way a mix between our object-oriented and functional counters. The implementation of the counter reads like a function: we start with n being 1 and then loop forever, yielding the counter value and incrementing it. On the other hand, the code generated by the compiler is object-oriented: our counter is actually an IterableIterator<number>, and we call next() on it to get the next value.

即使我们用一条while (true)语句来实现它,我们也不会陷入无限循环;该函数保持产生值并在每次之后被暂停yield。在幕后,编译器将我们编写的代码翻译成看起来更像我们以前的实现的代码。

Even though we implement this with a while (true) statement, we don’t get stuck in an infinite loop; the function keeps yielding values and gets suspended after each yield. Behind the scenes, the compiler translates the code we wrote into something that looks more like our previous implementations.

这个函数的类型是() => IterableIterator<number>. 请注意,它是一个生成器这一事实不会影响它的类型。没有参数但会返回 的函数IterableIterator<number>将具有完全相同的类型。*编译器使用声明来允许语句yield,但对类型系统是透明的。

The type of this function is () => IterableIterator<number>. Notice that the fact that it is a generator doesn’t affect its type. A function with no arguments that would return an IterableIterator<number> would have exactly the same type. The * declaration is used by the compiler to allow yield statements but is transparent to the type system.

我们将在后面的章节中回到迭代器和生成器并详细讨论它们。

We will come back to iterators and generators in a later chapter and discuss them at length.

6.2.4.计数器实现回顾

6.2.4. Counter implementations recap

在继续之前,让我们快速回顾一下实现计数器的四种方法和我们学到的各种语言特性:

Before moving on, let’s quickly recap the four ways to implement a counter and the various language features we learned about:

  • 全局计数器被实现为引用全局变量的简单函数。这个计数器有多个缺点:计数器值没有正确封装,我们不能有两个独立的计数器实例。
  • A global counter is implemented as a simple function that references a global variable. This counter has multiple drawbacks: the counter value is not properly encapsulated, and we cannot have two separate instances of the counter.
  • 面向对象的计数器实现很简单:计数器值是私有状态,我们公开一个next()方法来读取和递增它。大多数语言都会声明一个接口Iterable来支持此类场景,并提供语法糖来使用它们。
  • The object-oriented counter implementation is straightforward: the counter value is private state, and we expose a next() method to read and increment it. Most languages declare an interface like Iterable to support such scenarios and provide syntactic sugar to consume them.
  • 功能计数器是返回函数的函数。返回的函数是一个计数器。此实现利用 lambda 捕获来保存计数器的状态。代码比面向对象的版本更简洁。
  • A functional counter is a function that returns a function. The returned function is a counter. This implementation leverages lambda captures to hold the state of the counter. The code is more succinct than the object-oriented version.
  • 生成器使用特殊语法来创建可恢复函数。生成器不会返回,而是产生;它为调用者提供了一个值,但也会跟踪它的位置并在后续调用中从那里获取。生成器函数必须返回一个可迭代的迭代器。
  • A generator employs special syntax to create a resumable function. Instead of returning, a generator yields; it provides a value to the caller but also keeps track of where it was and picks up from there on subsequent calls. A generator function must return an iterable iterator.

接下来,我们将了解函数类型的另一个常见应用:异步函数。

Next, we’ll look at another common application of function types: asynchronous functions.

6.2.5.练习

6.2.5. Exercises

1个

实现一个函数,该函数在使用闭包调用时返回斐波那契数列中的下一个数字。

1

Implement a function that returns the next number in the Fibonacci sequence whenever it is called by using a closure.

2个

实现一个函数,该函数在使用生成器调用时返回斐波那契数列中的下一个数字。

2

Implement a function that returns the next number in the Fibonacci sequence whenever it is called by using a generator.

6.3. 异步执行长时间运行的操作

6.3. Executing long-running operations asynchronously

我们希望我们的应用程序尽可能快速和响应迅速,即使某些操作需要更长的时间才能完成。按顺序运行我们所有的代码可能会引入不可接受的延迟。如果我们因为等待下载完成而无法响应用户点击按钮,用户会感到沮丧。

We want our applications to be as fast and responsive as possible, even when certain operations take longer to complete. Running all our code sequentially might introduce unacceptable delays. If we can’t respond to our users clicking a button because we’re waiting for a download to complete, the users get frustrated.

通常,我们不想等待一个长时间运行的操作来执行一个更快的操作。最好异步执行此类长时间运行的任务,这样我们就可以在下载完成时保持 UI 交互。异步执行方式这些操作不会按照它们在代码中出现的顺序一个接一个地运行。它们可以并行运行,但这不是强制性的。JavaScript 是单线程的,所以异步执行是通过带有事件循环的运行时来实现的。我们将对使用多线程的并行执行和使用单线程的基于事件循环的执行进行高级描述,但首先,让我们看一个示例,其中异步运行代码会派上用场。

In general, we don’t want to wait for a long-running operation to execute a faster operation. It’s best to execute such long-running tasks asynchronously so we can keep the UI interactive while our download completes. Asynchronous execution means that the operations don’t run one after another, in the order in which they show up in the code. They could be running in parallel, but that’s not mandatory. JavaScript is single-threaded, so asynchronous execution is achieved by the run time with an event loop. We’ll go over a high-level description of both parallel execution using multiple threads and event loop–based execution with a single thread, but first, let’s look at an example in which running code asynchronously comes in handy.

假设我们要执行两个操作:问候我们的用户并将他们带到www.weather.com以便他们可以看到今天的天气。我们将使用两个函数来完成此操作:一个greet()询问用户姓名并向他们打招呼的函数,以及一个weather()启动浏览器了解今天天气的函数。让我们看一下同步实现,然后将其与异步实现进行对比。

Suppose that we want to perform two operations: greet our users and take them to www.weather.com so that they can see today’s weather. We’ll do this with two functions: a greet() function that asks for the user’s name and greets them, and a weather() function, which launches a browser for today’s weather. Let’s look at a synchronous implementation and then contrast it with an asynchronous one.

6.3.1.同步执行

6.3.1. Synchronous execution

我们将greet()使用 node 包来实现,如清单 6.9readline-sync所示。这个包提供了一种通过函数读取输入的方法。该函数返回用户键入的字符串。执行阻塞,直到用户键入他们的答案并按下回车键。我们可以使用安装包。 stdinquestion()npm install –save readline-sync

We will implement greet() by using the readline-sync node package, as shown in listing 6.9. This package provides a way to read input from stdin with the question() function. The function returns the string typed by the user. Execution blocks until the user types their answer and presses return. We can install the package with npm install –save readline-sync.

为了实现weather(),我们将使用openNode 包,它允许我们在浏览器中启动 URL。我们可以使用安装包npm install -save open

To implement weather(), we will use the open Node package, which allows us to launch a URL in the browser. We can install the package with npm install -save open.

清单 6.9。同步执行
函数问候():无效{
    const readlineSync = require('readline-sync');

    let name: string = readlineSync.question("你叫什么名字?");    1个
    console.log(`嗨 ${name}!`);
}

功能天气():无效{
    const open = require('打开');
    打开('https://www.weather.com/');
}

迎接();                                                                2
天气();                                                              2个
function greet(): void {
    const readlineSync = require('readline-sync');

    let name: string = readlineSync.question("What is your name? ");    1
    console.log(`Hi ${name}!`);
}

function weather(): void {
    const open = require('open');
    open('https://www.weather.com/');
}

greet();                                                                2
weather();                                                              2

  • 1 调用 question() 会阻止执行,直到用户输入他们的答案。
  • 1 Calling question() blocks execution until the user enters their answer.
  • 2 我们先调用greet(); 然后我们调用 weather()。
  • 2 We first call greet(); then we call weather().

让我们逐步了解运行此代码时发生的情况。首先,greet()被称为,我们要求用户给我们他们的名字。执行在这里停止,直到我们收到用户的回复,然后它继续输出问候语。返回后greet()weather()调用,启动www.weather.com

Let’s step through what happens when we run this code. First, greet() is called, and we ask the user to give us their name. Execution stops here until we receive a reply from the user, after which it proceeds by outputting a greeting. After greet() returns, weather() is called, launching www.weather.com.

此实现有效,但不是最佳的。这两个功能——问候用户和带他们到一个网站——在这种情况下是独立的,因此在另一个完成之前不应阻止其中一个。我们可以以不同的顺序调用这些函数,因为在这种情况下,很明显请求用户输入比启动应用程序花费的时间更长。但在实践中,我们不能总是分辨出两个功能中哪一个需要更长的时间才能完成。更好的方法是异步运行函数。

This implementation works, but it’s not optimal. The two functions—greeting the user and taking them to a website—are independent in this case, so one of them shouldn’t be blocked until the other one finishes. We could call the functions in a different order, because in this case, it’s obvious that requesting user input takes longer than launching an application. But in practice, we can’t always tell which one of two functions will take longer to complete. A better approach is to run the functions asynchronously.

6.3.2.异步执行:回调

6.3.2. Asynchronous execution: callbacks

的异步版本会greet()提示用户输入他们的姓名,但不会阻塞并等待回复。执行将通过调用继续weather()。我们仍然想在用户输入姓名后打印出来,所以我们需要一种方法来通知他们的回答。这是通过回调完成的。

An asynchronous version of greet() prompts the user for their name but does not block and wait for the reply. Execution will continue by calling weather(). We still want to print the user’s name after they enter it, so we need a way to be notified of their answer. This is done with a callback.

回调是我们作为参数提供给异步函数函数。异步函数不会阻塞执行;下一行代码被执行。当长时间运行的操作完成时(在这种情况下,等待用户回答他们的名字),回调函数被执行,所以我们可以处理结果。

A callback is a function that we provide to an asynchronous function as an argument. The asynchronous function does not block execution; the next line of code gets executed. When the long-running operation completes (in this case, waiting for the user to answer with their name), the callback function is executed, so we can handle the result.

greet()让我们看看下一个清单中的异步实现。我们将使用readlineNode.js 提供的模块。在这种情况下,该question()函数不会阻止执行;相反,它将回调作为参数。

Let’s see the asynchronous greet() implementation in the next listing. We will use the readline module provided by Node. In this case, the question() function does not block execution; rather, it takes a callback as an argument.

清单 6.10。带回调的异步执行
函数问候():无效{
    const readline = require( 'readline' );                     1个

    const rl = readline.createInterface({                      2
        输入:process.stdin,
        输出:process.stdout
    });

    rl.question("你叫什么名字?", (name: string) => {     3
         console.log(`Hi ${name}!`); 
        rl.close(); 
    } );
}

功能天气():无效{
    const open = require('打开');
    打开('https://www.weather.com/');
}

迎接();
天气();
function greet(): void {
    const readline = require('readline');                     1

    const rl = readline.createInterface({                     2
        input: process.stdin,
        output: process.stdout
    });

    rl.question("What is your name? ", (name: string) => {    3
        console.log(`Hi ${name}!`);
        rl.close();
    });
}

function weather(): void {
    const open = require('open');
    open('https://www.weather.com/');
}

greet();
weather();

  • 1 使用 readline 而不是 readline-sync
  • 1 Using readline instead of readline-sync
  • 2 createInterface() 是 readline 所需的额外设置,对我们的示例并不重要。
  • 2 createInterface() is extra setup required by readline and not important for our example.
  • 3 回调是一个 lambda,它将接收名称并打印出来。
  • 3 The callback is a lambda that will receive the name and print it.

逐步执行此程序,一旦question()调用并提示用户,就会继续执行而不等待用户的回答,从返回greet()并调用weather()What is your name?运行此程序会在终端上打印“ ”“42”,但www.weather.com将在用户提供答案之前打开。

Stepping through this program, as soon as question() is called and the user is prompted, execution continues without waiting for the user’s answer, returning from greet() and calling weather(). Running this program prints “What is your name?”“42” on the terminal, but www.weather.com will be open before the user provides their answer.

当有答案时,lambda 被调用。lambda 使用 将问候语打印到屏幕上,并console.log()使用 关闭交互式会话(以便不再请求用户输入)rl.close()

When an answer comes in, the lambda gets called. The lambda prints the greeting to the screen with console.log() and closes the interactive session (so that no more user input is requested) with rl.close().

6.3.3.异步执行模型

6.3.3. Asynchronous execution models

正如本节开头简要提到的,可以使用线程或事件循环来实现异步执行。选择取决于您的运行时和您使用的库如何实现异步操作。在 JavaScript 中,异步执行是通过事件循环实现的。

As briefly mentioned at the start of this section, asynchronous execution can be achieved with threads or with an event loop. The choice depends on how your run time and the library you are using implement asynchronous operations. In JavaScript, asynchronous execution is implemented with an event loop.

线程

每个应用程序都作为一个进程运行。一个进程从一个主线程开始,但我们可以创建多个其他线程来运行代码。在符合 POSIX 标准的系统(例如 Linux 和 macOS)上,新线程使用 来创建pthread_create(),而 Windows 则提供CreateThread(). 这些 API 由操作系统提供。编程语言为库提供了不同的接口,但这些库最终在内部使用操作系统 API。

Each application runs as a process. A process starts with a main thread, but we can create multiple other threads on which to run code. On POSIX-compliant systems such as Linux and macOS, new threads are created with pthread_create(), whereas Windows provides CreateThread(). These APIs are provided by the operating systems. Programming languages provide libraries with different interfaces, but those libraries end up using the OS APIs internally.

不同的线程可以同时运行。多个 CPU 内核可以并行执行指令,每个处理一个不同的线程。如果线程数大于硬件可以并行运行的数量,操作系统会确保每个线程获得相当多的运行时间。线程调度程序暂停和恢复线程以实现此结果。线程调度器是操作系统内核的核心组件。

Separate threads can run at the same time. Multiple CPU cores can execute instructions in parallel, each handling a different thread. If the number of threads is larger than the hardware can run in parallel, the operating system ensures that each thread gets a fair amount of run time. Threads get paused and resumed by the thread scheduler to achieve this result. The thread scheduler is a core component of the OS kernel.

我们不会查看线程的代码示例,因为 JavaScript(以及 TypeScript)在历史上一直是单线程的。Node 最近启用了对工作线程的实验性支持,但在撰写本文时,这种开发还相当新。也就是说,如果您使用任何其他主流语言进行编程,您可能熟悉如何创建新线程并在其上并行执行代码(图 6.6)。

We won’t look at a code sample for threads, as JavaScript (and, thus, TypeScript) has been historically single-threaded. Node recently enabled experimental support for worker threads, but this development is fairly recent at the time of this writing. That being said, if you program in any other mainstream language, you are probably familiar with how to create new threads and execute code on them in parallel (figure 6.6).

图 6.6。createThread()创建一个新线程。原线程继续执行operation1()然后operation2(),新线程longRunningOperation()并行执行。

事件循环

多线程的替代方法是事件循环。事件循环使用队列:异步函数入队,它们自己可以入队其他函数。只要队列不为空,队列中的第一个函数就会出列并执行。

An alternative to multiple threads is an event loop. An event loop uses a queue: asynchronous functions get enqueued, and they themselves can enqueue other functions. As long as the queue is not empty, the first function in line gets dequeued and executed.

作为示例,让我们看一下从给定数字开始倒数的函数,如以下清单所示。该函数不会在倒计时完成之前阻塞执行,而是使用事件队列并将对自身的另一个调用排入队列,直到它达到 0(图 6.7)。

As an example, let’s look at a function that counts down from a given number, shown in the following listing. Instead of blocking execution until the countdown is complete, this function will use an event queue and enqueue another call to itself until it reaches 0 (figure 6.7).

图 6.7。countDown()计算一步;然后它产生并允许其他代码运行。它还使用countDown()递减的计数器值对另一个调用进行排队。如果计数器达到0countDown()则不会将对自身的另一个调用加入队列。

清单 6.11。在事件循环中倒计时
类型 AsyncFunction = () => void;                             1个

让队列:AsyncFunction[] = [];                             2个

函数 countDown(counterId: string, from: number): void {
    console.log(`${counterId}: ${from}`);                    3个

    如果(从 > 0)
        queue.push(() => countDown(counterId, from - 1));    4个
}

queue.push(() => countDown('counter1', 4));                  5个

while (queue.length > 0) {                                    6
    让 func: AsyncFunction = <AsyncFunction>queue.shift();
    功能();
}
type AsyncFunction = () => void;                             1

let queue: AsyncFunction[] = [];                             2

function countDown(counterId: string, from: number): void {
    console.log(`${counterId}: ${from}`);                    3

    if (from > 0)
        queue.push(() => countDown(counterId, from - 1));    4
}

queue.push(() => countDown('counter1', 4));                  5

while (queue.length > 0) {                                   6
    let func: AsyncFunction = <AsyncFunction>queue.shift();
    func();
}

  • 1 我们将我们的异步函数限制为不带返回 void 的参数的函数。
  • 1 We’ll restrict our asynchronous functions to functions without arguments that return void.
  • 2 我们的队列将是一个函数数组。
  • 2 Our queue will be an array of functions.
  • 3 计数器打印其 ID 和当前值。
  • 3 The counter prints its id and current value.
  • 4 如果大于 0,则计数器将另一个对 countDown() 的调用排入队列,从而递减值。
  • 4 If greater than 0, the counter enqueues another call to countDown(), decrementing the value.
  • 5 我们通过排队从 4 调用 countDown() 来启动该过程。
  • 5 We kick off the process by queueing a call to countDown() from 4.
  • 6 当队列中有一个函数时,将其出队并调用它。
  • 6 While there is a function in the queue, dequeue it and call it.

此代码将输出

This code will output

柜台1:4
柜台 1: 3
柜台 1: 2
计数器 1:1
计数器 1:0
counter1: 4
counter1: 3
counter1: 2
counter1: 1
counter1: 0

当计数器达到 时0,它不会将另一个呼叫加入队列,因此程序将停止。到目前为止,这并不比简单地在循环中计数更有趣。但是,如果我们首先让两个计数器排队,会发生什么情况?

When the counter reaches 0, it will not enqueue another call, so the program will stop. So far, this isn’t much more interesting than simply counting in a loop. But what happens if we start by enqueuing two counters?

清单 6.12。事件循环中的两个计数器
类型 AsyncFunction = () => void;

让队列:AsyncFunction[] = [];

函数 countDown(counterId: string, from: number): void {
    console.log(`${counterId}: ${from}`);

    如果(从 > 0)
        queue.push(() => countDown(counterId, from - 1));
}

queue.push(() => countDown('counter1', 4));
queue.push(() => countDown('counter2', 2));       1个

while (queue.length > 0) {
    让 func: AsyncFunction = <AsyncFunction>queue.shift();
    功能();
}
type AsyncFunction = () => void;

let queue: AsyncFunction[] = [];

function countDown(counterId: string, from: number): void {
    console.log(`${counterId}: ${from}`);

    if (from > 0)
        queue.push(() => countDown(counterId, from - 1));
}

queue.push(() => countDown('counter1', 4));
queue.push(() => countDown('counter2', 2));      1

while (queue.length > 0) {
    let func: AsyncFunction = <AsyncFunction>queue.shift();
    func();
}

  • 1 与前面示例的唯一区别是现在我们将第二个计数器排入队列。
  • 1 The only difference from the preceding sample is that now we enqueue a second counter.

这一次,输出是

This time around, the output is

柜台1:4
柜台 2:2
柜台 1: 3
柜台 2: 1
柜台 1: 2
计数器 2:0
计数器 1:1
计数器 1:0
counter1: 4
counter2: 2
counter1: 3
counter2: 1
counter1: 2
counter2: 0
counter1: 1
counter1: 0

正如我们所见,这次计数器是交错的。每个计数器递减一个步骤;然后另一个有机会计数。如果只是循环倒计时,我们是达不到这个结果的。使用队列,这两个函数在倒计时的每一步后产生,并允许其他代码在它们再次倒计时之前运行。

As we can see, this time the counters are interleaved. Each counter counts down one step; then the other one gets a chance to count. We couldn’t achieve this result if we just counted down in a loop. Using the queue, the two functions yield after each step of the countdown and allow other code to run before they count down again.

这两个计数器不会同时运行;要么counter1或者counter2得到一些时间来运行。但它们确实彼此异步或独立运行。他们中的任何一个都可以先完成执行,而不管另一个需要多长时间(图 6.8)。

The two counters do not run at the same time; either counter1 or counter2 gets some time to run. But they do run asynchronously, or independently, of each other. Either of them can finish execution first, regardless of how much longer the other one takes (figure 6.8).

图 6.8。每个计数器运行,然后将另一个操作排入队列。执行按照操作入队的顺序进行。一切都在一个线程上运行。

对于等待输入的操作,例如来自键盘的操作,运行时可以确保处理该输入的操作仅在收到输入后才排队,在这种情况下,其他代码可以在提供输入时运行。这样,需要输入的长时间运行的操作可以拆分为两个运行时间较短的操作;第一个请求输入并返回,第二个在输入到达时处理输入。运行时处理在输入可用后安排第二个操作。

For operations that wait for input, such as from the keyboard, the run time can ensure that an operation to handle that input is queued only after input is received, in which case other code can run while the input is being provided. This way, a long--running operation that requires input can be split into two shorter-running ones; the first requests input and returns, and the second processes input when it arrives. The run time handles scheduling the second operation after input is available.

对于无法拆分为多个块的长时间运行的操作,事件循环效果不佳。如果我们排队一个不会产生并运行很长时间的操作,事件循环将被卡住直到它完成。

Event loops don’t work as well for long-running operations that cannot be split into multiple chunks. If we enqueue an operation that doesn’t yield and runs for a long time, the event loop will be stuck until it finishes.

6.3.4.异步函数回顾

6.3.4. Asynchronous functions recap

如果我们同步执行长时间运行的操作,则在长时间运行的操作完成之前不会运行其他代码。输入/输出操作是长时间运行操作的好例子,因为从磁盘或网络读取比从内存读取具有更高的延迟。

If we execute long-running operations synchronously, no other code runs until the long-running operation completes. Input/output operations are good examples of long-running operations, as reading from disk or from the network has higher latency than reading from memory.

我们可以异步执行这些操作,而不是同步执行这些操作,并提供一个回调函数,以便在长时间运行的操作完成时调用。执行异步代码有两种主要模型:一种使用多线程,另一种使用事件循环。

Instead of executing such operations synchronously, we can execute them asynchronously and provide a callback function to be called when the long-running operation completes. There are two main models of executing asynchronous code: one that uses multiple threads and one that uses an event loop.

线程可以在单独的 CPU 内核上并行运行,这是它们的主要优势,因为不同的代码片段可以同时运行,并且整个程序可以更快地完成。一个缺点是同步开销:在线程之间传递数据需要小心同步。我们不会在本书中讨论该主题,但您可能听说过诸如死锁活锁之类的问题,其中两个线程永远不会完成,因为它们相互等待。

Threads can run in parallel on separate CPU cores, which is their main advantage, as different pieces of code can run at the same time, and the overall program finishes faster. A drawback is the synchronization overhead: passing data between threads requires careful synchronization. We won’t cover the topic in this book, but you’ve probably heard of problems such as deadlock and livelock, in which two threads never complete because they wait on each other.

事件循环在单个线程上运行,但启用了一种机制,可以在等待输入时将长时间运行的代码放在队列的后面。使用事件循环的优点是它不需要同步,因为一切都在单个线程上运行。缺点是虽然在等待数据时排队 I/O 操作工作正常,但 CPU 密集型操作仍然阻塞。CPU 密集型操作,如复杂计算,不能只排队;因为它不是在等待数据,所以它需要 CPU 周期。线程更适合这项任务。

An event loop runs on a single thread but enables a mechanism to put long--running code at the back of the queue while it awaits input. The advantage of using an event loop is that it doesn’t require synchronization, as everything runs on a single thread. The disadvantage is that although queuing up I/O operations as they wait for data works fine, CPU-intensive operations still block. A CPU-intensive operation, like a complex computation, can’t just be queued; as it’s not waiting for data, it requires CPU cycles. Threads are much better suited to this task.

大多数主流编程语言都使用线程,JavaScript 是一个明显的例外。话虽这么说,甚至 JavaScript 也在扩展以支持 Web 工作线程(在浏览器中运行的后台线程),并且 Node 对浏览器外的类似工作线程有实验性支持。

Most mainstream programming languages use threads, JavaScript being a notable exception. That being said, even JavaScript is being extended with support for web worker threads (background threads running in the browser), and Node has experimental support for similar worker threads outside the browser.

在下一节中,我们将研究如何使异步代码更清晰、更易于阅读。

In the next section, we look at how we can make our asynchronous code cleaner and easier to read.

6.3.5.练习

6.3.5. Exercises

1个

以下哪项可用于实现异步执行模型?

  1. 线程
  2. 事件循环
  3. 既不是a也不是b
  4. a和b都

1

Which of the following can be used to implement an asynchronous execution model?

  1. Threads
  2. An event loop
  3. Neither a nor b
  4. Both a and b

2个

在基于事件循环的异步系统中,两个函数可以同时执行吗?

  1. 是的

2

Can two functions execute at the same time in an event-loop-based asynchronous system?

  1. Yes
  2. No

3个

在基于线程的异步系统中可以同时执行两个函数吗?

  1. 是的

3

Can two functions execute at the same time in a thread-based asynchronous system?

  1. Yes
  2. No

6.4. 简化异步代码

6.4. Simplifying asynchronous code

回调的工作方式与前面示例中的计数器相同。计数器在每次运行后将对自身的另一个调用排入队列,而异步函数可以将另一个函数作为参数并在完成执行时将对该函数的调用排入队列。

Callbacks work in the same way as our counter in the preceding example. Whereas the counter enqueues another call to itself after each run, an asynchronous function can take another function as an argument and enqueue a call to that function when it completes execution.

例如,让我们在下一个列表中使用一个在计数器到达后排队的回调来增强我们的计数器0

As an example, let’s enhance our counter in the next listing with a callback that gets queued after the counter reaches 0.

清单 6.13。带回调的计数器
function countDown(counterId: string, from: number,
     callback: () => void ): void {                          1
    console.log(`${counterId}: ${from}`);

    如果(从 > 0)
        queue.push(() => countDown(counterId, from – 1, callback));
    否则                                                  2
         queue.push(回调);                              2个
}

queue.push(() => countDown('counter1', 4,
    () => console.log('完成') ));                          3个
function countDown(counterId: string, from: number,
    callback: () => void): void {                         1
    console.log(`${counterId}: ${from}`);

    if (from > 0)
        queue.push(() => countDown(counterId, from – 1, callback));
    else                                                  2
        queue.push(callback);                             2
}

queue.push(() => countDown('counter1', 4,
    () => console.log('Done')));                          3

  • 1 我们添加回调参数,它是一个没有参数且返回 void 的函数。
  • 1 We add the callback argument, which is a function with no arguments that returns void.
  • 2 当我们完成倒计时后,我们将要执行的回调排队。
  • 2 When we’re done counting down, we queue the callback to be executed.
  • 3 我们提供一个回调函数,在计数器完成时打印“Done”。
  • 3 We provide a callback that prints “Done” when the counter completes.

回调是处理异步代码的常见模式。在我们的示例中,我们使用了不带参数的回调,但回调也可以从异步函数接收参数。question()来自模块的异步调用就是这种情况readline,它将用户提供的字符串传递给回调。

Callbacks are a common pattern for dealing with asynchronous code. In our example, we used a callback without arguments, but callbacks can also receive arguments from the asynchronous function. That was the case with our asynchronous question() call from the readline module, which passed the string provided by the user to the callback.

用回调链接多个异步函数会导致很多嵌套函数,正如我们在代码清单 6.14中看到的,我们想用一个getUserName()函数询问用户姓名,用一个getUser-Birthday()函数询问他们的生日,询问他们的电子邮件地址等等. 这些函数相互依赖,因为它们中的每一个都需要前一个函数的一些信息。(getUser-Birthday()例如,需要用户名。)每个函数也是异步的,因为它可能会长时间运行,所以它需要一个回调来提供其结果。我们使用这些回调来调用链中的下一个函数。

Chaining multiple asynchronous functions with callbacks leads to a lot of nested functions, as we can see in listing 6.14, in which we want to ask the user’s name with a getUserName() function, ask their birthday with a getUser-Birthday() function, ask their email address, and so on. The functions depend on one another because each of them requires some information from the preceding one. (getUser-Birthday() requires the user’s name, for example.) Each function is also asynchronous, as it is potentially long-running, so it takes a callback to provide its result. We use these callbacks to call the next function in the chain.

清单 6.14。链接回调
声明函数 getUserName(
    回调:(名称:字符串)=>无效:无效;            1个
声明函数 getUserBirthday(name: string,
    回调:(生日:日期)=>无效:无效;          1个
声明函数 getUserEmail(生日: 日期,
    回调:(电子邮件:字符串)=>无效:无效;           1个

getUserName((名称: 字符串) => {
    console.log(`嗨 ${name}!`);
    getUserBirthday(名字, (生日: 日期) => {          2
        今天常量:Date = new Date();
        如果(生日.getMonth()==今天.getMonth()&&
            生日.getDay() == 今天.getDay())
            console.log('生日快乐!');

        getUserEmail(生日, (email: string) => {      3
            /* ... */
        });
    })
});
declare function getUserName(
    callback: (name: string) => void): void;            1
declare function getUserBirthday(name: string,
    callback: (birthday: Date) => void): void;          1
declare function getUserEmail(birthday: Date,
    callback: (email: string) => void): void;           1

getUserName((name: string) => {
    console.log(`Hi ${name}!`);
    getUserBirthday(name, (birthday: Date) => {         2
        const today: Date = new Date();
        if (birthday.getMonth() == today.getMonth() &&
            birthday.getDay() == today.getDay())
            console.log('Happy birthday!');

        getUserEmail(birthday, (email: string) => {     3
            /* ... */
        });
    })
});

  • 1 我们不会为这些函数提供实现——只是声明。
  • 1 We won’t provide implementations for these functions—just declarations.
  • 2 getUserName() 的回调调用 getUserBirthday()。
  • 2 The callback to getUserName() calls getUserBirthday().
  • 3 getUserBirthday() 的回调调用getUserEmail() 等。
  • 3 The callback to getUserBirthday() calls getUserEmail() and so on.

getUserName()在获取名称时调用的回调中,我们调用getUserBirthday(),将名称传递给它。getUserBirthday()在获取生日时调用的回调中,我们调用getUserEmail()传入生日等。

In the callback invoked when getUserName() obtains the name, we call getUserBirthday(), passing it the name. In the callback invoked when getUserBirthday() obtains the birthday, we call getUserEmail() passing in the birthday and so on.

我们不会在本例中详细介绍所有功能的实际实现,因为它们与上一节中的实现getUser...类似。greet()我们在这里更关心调用代码的整体结构。以这种方式构建代码使其难以阅读,因为我们将更多的回调链接在一起,我们最终在 lambda 中嵌套了更多的 lambda。事实证明,对于这种异步函数调用模式有更好的抽象:promises。

We won’t go over the actual implementation of all the getUser... functions in this example, as they would be similar to the greet()implementation in the preceding section. We’re more concerned here with the overall structure of the calling code. Structuring code this way makes it hard to read, as the more callbacks we chain together, the more nested lambdas inside lambdas we end up with. It turns out that there is a better abstraction for this pattern of asynchronous function calls: promises.

6.4.1.链接承诺

6.4.1. Chaining promises

我们首先观察这样一个函数getUserName(callback: (name: string) => void)是一个异步函数,它将在某个时间点确定用户的姓名,然后将其交给我们提供的回调。换句话说,getUserName()“承诺”最终会返回一个名称字符串。我们还观察到,只要函数具有承诺值,我们就希望它调用另一个函数,将该值作为参数传递。

We start by observing that a function such as getUserName(callback: (name: string) => void) is an asynchronous function that will, at some point in time, determine the user’s name and then hand it over to a callback we provide. In other words, getUserName() “promises” to give back a name string eventually. We also observe that whenever the function has the promised value, we want it to call another function, passing that value as an argument.

承诺和延续

Promise是在未来某个时间点可用的值的代理。在产生值的代码运行之前,其他代码可以使用 promise 来设置值到达时如何处理、发生错误时如何处理,甚至取消未来的执行。当 promise 的结果可用时设置为调用的函数称为continuation

A promise is a proxy for a value that will be available at a future point in time. Until the code that produces the value runs, other code can use the promise to set up how the value will be processed when it arrives, what to do in case of error, and even to cancel the future execution. A function set up to be called when the result of a promise is available is called a continuation.

promise 的两个主要成分是T我们的函数“承诺”给我们的某种类型的值,以及将函数指定为T其他类型U( (value: T) => U) 的能力,当 promise 实现并且我们有我们的价值时调用(延续)。这是直接向函数提供回调的替代方法。

The two main ingredients of a promise are a value of some type T that our function “promises” to give us and the ability to specify a function from T to some other type U ((value: T) => U), to be called when the promise is fulfilled and we have our value (a continuation). This is an alternative to supplying the callback directly to a function.

首先,让我们更新清单 6.15中函数的声明,这样它们就不会采用回调参数,而是返回一个Promise. getUserName()将返回一个Promise-<string>getUserBirthday()将返回一个Promise<Date>,并将getUser-Email()返回另一个Promise<string>

First, let’s update the declarations of our functions in listing 6.15 so that instead of taking a callback argument, they return a Promise. getUserName() will return a Promise-<string>, getUserBirthday() will return a Promise<Date>, and getUser-Email() will return another Promise<string>.

清单 6.15。函数返回承诺
声明函数 getUserName(): Promise<string>;
声明函数 getUserBirthday(name: string): Promise<Date>;
声明函数 getUserEmail(birthday: Date): Promise<string>;
declare function getUserName(): Promise<string>;
declare function getUserBirthday(name: string): Promise<Date>;
declare function getUserEmail(birthday: Date): Promise<string>;

JavaScript(以及 TypeScript)提供了一个内置Promise<T>类型来实现这种抽象。在 C# 中,Task<T>实现了这一点,在 Java 中,CompletableFuture<T>提供了类似的功能。

JavaScript (and, thus, TypeScript) provides a built-in Promise<T> type that implements this abstraction. In C#, Task<T> implements this, and in Java, CompletableFuture<T> provides similar functionality.

promise 提供了一种then()允许我们传递延续的方法。每个then()函数返回另一个承诺,因此我们可以将调用then()链接在一起。这个过程消除了我们在基于回调的实现中看到的嵌套。

A promise provides a then() method that allows us to pass in our continuation. Each then() function returns another promise, so we can chain then() calls together. This process eliminates the nesting we saw in the callback-based implementation.

清单 6.16。链接承诺
获取用户名()
    .then((名称: 字符串) => {                      1
        console.log(`嗨 ${name}!`);
        返回 getUserBirthday(名字);             2个
    })
    .then((生日: 日期) => {                    3
        今天常量:Date = new Date();
        如果(生日.getMonth()==今天.getMonth()&&
            生日.getDay() == 今天.getDay())
            console.log('生日快乐!');
        返回 getUserEmail(生日);
    })
    .then((email: string) => {                     4
        /* ... */
    });
getUserName()
    .then((name: string) => {                     1
        console.log(`Hi ${name}!`);
        return getUserBirthday(name);             2
    })
    .then((birthday: Date) => {                   3
        const today: Date = new Date();
        if (birthday.getMonth() == today.getMonth() &&
            birthday.getDay() == today.getDay())
            console.log('Happy birthday!');
        return getUserEmail(birthday);
    })
    .then((email: string) => {                    4
        /* ... */
    });

  • 1 我们根据 getUserName() 返回的承诺调用 then()。
  • 1 We call then() on the promise returned by getUserName().
  • 2 在此延续中,我们使用 getUserName() 提供的值。
  • 2 In this continuation, we use the value provided by getUserName().
  • 3 因为 then() 返回另一个 promise,我们可以再次对返回值调用 then()。. .
  • 3 Because then() returns another promise, we can call then() on the returned value again . . .
  • 4 . . . 然后再次。
  • 4 . . . and again.

正如我们所看到的,不是在回调中的回调中有回调,而是以一种更容易遵循的模式将延续链接在一起:我们运行一个函数,then()我们运行另一个函数,等等。

As we can see, instead of having a callback within a callback within a callback, continuations are chained together in a pattern that’s easier to follow: we run a function, then() we run another function, and so on.

6.4.2.创造承诺

6.4.2. Creating promises

如果我们想使用这个模式,我们还应该看看如何创建一个承诺。原理很简单,尽管它依赖于高阶函数——一个 promise 将一个函数作为参数,该函数将另一个函数作为参数——所以乍一看似乎令人费解。

If we want to use this pattern, we should also look at how we can create a promise. The principle is straightforward, though it relies on higher-order functions—a promise takes as argument a function that takes as argument another function—so it may seem mind-bending at first.

对某种类型的值(例如 )的承诺Promise<string>并不知道如何计算该值。它then()为我们之前看到的continuation chaining 提供了一种方法,但是它不能确定字符串是什么。在 的情况下getUserName(),承诺的字符串是用户的姓名,在 的情况下getUserEmail(),承诺的字符串是电子邮件地址。那么,仿制药如何Promise<string>能够确定该值呢?答案是它不能没有帮助。promise 的构造函数将实际处理值计算的函数作为参数。对于getUserName(),该功能会提示用户输入他们的姓名并得到他们的回复。然后,promise 可以通过直接调用它来使用这个函数,将它排入事件循环队列,或者安排它的执行在一个线程上,取决于实现,它因语言和库而异。

A promise for a value of a certain type, such as Promise<string>, doesn’t really know how to compute that value. It provides a then() method for the continuation chaining we saw before, but it cannot determine what the string is. In the case of getUserName(), the promised string is the name of the user, and in the case of getUserEmail(), the promised string is an email address. How, then, could a generic Promise<string> be able to determine that value? The answer is that it can’t without help. The constructor of a promise takes as an argument a function that actually handles computing the value. For getUserName(), that function would prompt the user for their name and get their reply. The promise can then use this function by calling it directly, queuing it for the event loop, or scheduling its execution on a thread, depending on the implementation, which differs from language to language and library to library.

到目前为止,一切都很好。获取Promise<string>一些将提供值的代码。但是因为该代码可能会在稍后运行,我们还需要一种机制让该代码告诉 promise 值已经到达。对于该任务,promise 将传递一个调用resolve()该代码的函数。确定值后,代码可以调用resolve()并将值交还给承诺(图 6.9)。

So far, so good. The Promise<string> gets some code that will provide the value. But because that code might run at a later time, we also need a mechanism for that code to tell the promise that the value has arrived. For that task, the promise will pass a function called resolve() to that code. When the value is determined, the code can call resolve() and hand the value back to the promise (figure 6.9).

图 6.9。getUserName()将代码排入队列以获取用户名并返回一个Promise<string>. 的调用者getUserName()可以调用then()连接getUserEmail()延续代码的承诺——当我们有用户名时要运行的代码。稍后,获取用户名的代码运行并resolve()使用用户名进行调用。此时,getUserEmail()将使用现在可用的用户名调用延续。

让我们看看我们如何getUserName()在下一个清单中实现返回一个承诺。

Let’s look at how we can implement getUserName() in the next listing to return a promise.

清单 6.17。getUserName()回报承诺
函数 getUserName(): Promise<string> {
    返回新的 Promise<string>(
        (解析:(值:字符串)=> void)=> {                   1 
        const readline = require('readline');                    2个

        const rl = readline.createInterface({                     2
            输入:process.stdin,
            输出:process.stdout
        });

        rl.question("你叫什么名字?", (name: string) => {    2
            rl.close();
            解决(名称);                                       3个
        });
    });
}
function getUserName(): Promise<string> {
    return new Promise<string>(
        (resolve: (value: string) => void) => {                  1
        const readline = require('readline');                    2

        const rl = readline.createInterface({                    2
            input: process.stdin,
            output: process.stdout
        });

        rl.question("What is your name? ", (name: string) => {   2
            rl.close();
            resolve(name);                                       3
        });
    });
}

  • 1 我们将 lambda 传递给 Promise 构造函数,它期望将 resolve() 函数作为参数。
  • 1 We pass a lambda to the Promise constructor, which expects as argument a resolve() function.
  • 2 我们使用与 greet() 中相同的代码从标准输入中读取字符串。
  • 2 We use the same code as in greet() to read a string from stdin.
  • 3 最后,当我们有了名称时,我们调用提供的 resolve() 函数并将名称传递给它。
  • 3 Finally, when we have a name, we call the provided resolve() function and pass it the name.

getUserName()简单地创建并返回一个承诺。promise 是用一个带有resolvetype 参数的函数初始化的(value: string) => void。此函数包含要求用户提供其姓名的代码,提供姓名后,该函数将调用 resolve()以将值传递给承诺。

getUserName() simply creates and returns a promise. The promise is initialized with a function that takes a resolve argument of type (value: string) => void. This function contains the code to ask the user to provide their name, and when the name is provided, the function calls resolve() to pass the value to the promise.

如果我们实现长时间运行的函数来返回承诺,我们可以通过使用将这些异步调用链接在一起,Promise.then()使我们的代码更具可读性。

If we implement long-running functions to return promises, we can chain these asynchronous calls together by using Promise.then() to make our code more readable.

6.4.3.更多关于承诺

6.4.3. More about promises

承诺不仅仅是提供延续。让我们看看 promise 如何处理错误,以及除了使用then().

There’s more to promises than providing continuations. Let’s see how promises handle errors and a couple more ways to sequence their execution beyond using then().

处理错误

承诺可以处于三种状态之一:未决、已解决和已拒绝。Pending表示 promise 已创建但尚未解决(即负责提供值的 provided 函数尚未调用resolve())。已解决意味着resolve()已调用并提供了一个值,此时调用了延续。但是如果出现错误怎么办?当负责提供值的函数抛出异常时,promise 进入拒绝状态。

A promise can be in one of three states: pending, settled, and rejected. Pending means that the promise has been created but not yet resolved (that is, the provided function responsible for providing a value hasn’t called resolve() yet). Settled means that resolve() was called and a value is provided, at which point continuations are called. But what happens if there is an error? When the function responsible for providing a value throws an exception, the promise enters the rejected state.

事实上,负责为 promise 提供值的函数可以将附加函数作为参数,因此它可以将 promise 设置为拒绝状态并提供拒绝的原因。而不是提供

In fact, the function responsible for providing a value to the promise can take an additional function as an argument, so it can set the promise in the rejected state and provide a reason for that. Instead of providing

(解决:(值:T)=>无效)=>无效
(resolve: (value: T) => void) => void

对于构造函数,调用者可以提供一个

to the constructor, callers can provide a

(解决:(值:T)=>无效,拒绝:(原因:任何)=>无效)=>无效
(resolve: (value: T) => void, reject: (reason: any) => void) => void

第二个参数是一个函数(reason: any) => void,它可以为承诺提供任何类型的原因并将其标记为已拒绝。

The second argument is a function (reason: any) => void, which can provide a reason of any type to the promise and mark it as rejected.

即使不调用reject(),如果函数抛出异常,promise 也会自动认为自己被拒绝。除了then()函数之外,promise 还公开了一个catch()函数,我们可以在其中提供一个延续,当 promise 因任何原因被拒绝时被调用(图 6.10)。

Even without calling reject(), if the function throws an exception, the promise will automatically consider itself to be rejected. Besides the then() function, a promise exposes a catch() function in which we can provide a continuation to be called when the promise is rejected for whatever reason (figure 6.10).

图 6.10。一个 promise 从pending状态开始。(getUserName()计划代码以提示用户,但question()尚未返回。)resolve()将其转换为稳定状态并调用继续(如果提供)(在用户提供其姓名后)。一个值可用,因此可以调用延续(getUserEmail()在我们的例子中)。reject()将承诺转换为拒绝状态并调用错误处理延续(如果提供的话)。值不可用;相反,错误的原因是可用的。

让我们扩展我们的getUserName()函数以拒绝下一个列表中的空字符串。

Let’s extend our getUserName() function to reject an empty string in the next listing.

清单 6.18。拒绝承诺
函数 getUserName(): Promise<string> {
    const readline = require('readline');

    const rl = readline.createInterface({
        输入:process.stdin,
        输出:process.stdout
    });

    返回新的 Promise<string>(
        (解析:(值:字符串)=>无效,
         拒绝:(原因:字符串)=>无效)=> {                        1
        rl.question("你叫什么名字?", (name: string) => {
            rl.close();

            如果(名称。长度!= 0){
                解决(名称);
            } 别的 {
                reject("姓名不能为空");                         2个
            }
        });
    });
}

获取用户名()
    .then((name: string) => { console.log(`Hi ${name}!`); })
    .catch((reason: string) => { console.log(`Error: ${reason}`); });  3个
function getUserName(): Promise<string> {
    const readline = require('readline');

    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    return new Promise<string>(
        (resolve: (value: string) => void,
         reject: (reason: string) => void) => {                       1
        rl.question("What is your name? ", (name: string) => {
            rl.close();

            if (name.length != 0) {
                resolve(name);
            } else {
                reject("Name can't be empty");                        2
            }
        });
    });
}

getUserName()
    .then((name: string) => { console.log(`Hi ${name}!`); })
    .catch((reason: string) => { console.log(`Error: ${reason}`); }); 3

  • 1 我们提供额外的拒绝参数。
  • 1 We provide the additional reject argument.
  • 2 如果 name.length 为 0,我们拒绝承诺。
  • 2 If name.length is 0, we reject the promise.
  • 3 与 catch() 连接的新延续在拒绝时被调用(或者如果抛出异常)。
  • 3 The new continuation hooked up with catch() gets called on reject (or if an exception is thrown).

一个 promise 不仅会通过调用reject()或由于抛出错误而被拒绝,而且通过then()get rejected 链接到它的所有其他 promise 也会被拒绝。如果链中的任何承诺被拒绝,将调用在调用链 catch()末尾添加的延续。then()

Not only does a promise get rejected, either via a call to reject() or due to an error being thrown, but also all other promises chained to it via then() get rejected. A catch() continuation added at the end of a chain of then() calls will get called if any of the promises in the chain is rejected.

链接同步函数

将延续链接在一起的方法比我们目前所介绍的要多。首先,continuation 不必返回 promise。我们并不总是链接异步函数;也许延续是短期运行的,可以同步执行。让我们再看看下面清单中的原始示例,其中我们所有的延续都返回了承诺。

There are more ways to chain continuations together than what we’ve covered so far. First, a continuation doesn’t have to return a promise. We don’t always chain asynchronous functions; maybe the continuation is short-running and can be executed synchronously. Let’s take another look at our original example in the following listing, in which all our continuations returned promises.

清单 6.19。链接函数返回承诺
获取用户名()                                    1
    .then((名称: 字符串) => {
        console.log(`嗨 ${name}!`);
        返回 getUserBirthday(名字);            2个
    })
    .then((生日: 日期) => {
        今天常量:Date = new Date();
        如果(生日.getMonth()==今天.getMonth()&&
            生日.getDay() == 今天.getDay())
            console.log('生日快乐!');
        返回 getUserEmail(生日);          3个
    })
    .then((email: string) => {
        /* ... */
    });
getUserName()                                   1
    .then((name: string) => {
        console.log(`Hi ${name}!`);
        return getUserBirthday(name);           2
    })
    .then((birthday: Date) => {
        const today: Date = new Date();
        if (birthday.getMonth() == today.getMonth() &&
            birthday.getDay() == today.getDay())
            console.log('Happy birthday!');
        return getUserEmail(birthday);          3
    })
    .then((email: string) => {
        /* ... */
    });

  • 1 getUserName() 返回一个 Promise<string>。
  • 1 getUserName() returns a Promise<string>.
  • 2 getUserBirthday() 返回一个 Promise<Date>。
  • 2 getUserBirthday() returns a Promise<Date>.
  • 3 getUserEmail() 返回一个 Promise<string>。
  • 3 getUserEmail() returns a Promise<string>.

在这种情况下,我们所有的函数都需要异步运行,因为它们需要用户输入。但是,如果在我们获得用户名后,我们只是想将它拼接在一个字符串中并返回它呢?如果我们的延续只是return `Hi ${name}!`,它返回一个字符串,而不是一个承诺。但没关系;该then()函数会自动将其转换为 a Promise-<string>,以便可以由另一个延续进一步处理,如以下代码所示。

In this case, all our functions need to run asynchronously, as they expect user input. But what if after we get the user’s name, we simply want to splice it inside a string and return that? If our continuation is just return `Hi ${name}!`, it returns a string, not a promise. But that’s OK; the then() function automatically converts it in a Promise-<string> so that it can be further processed by another continuation, as shown in the following code.

清单 6.20。不返回承诺的链接函数
获取用户名()
    .then((名称: 字符串) => {
        返回`嗨${name}!`;        1个
    })
    .then((问候语:字符串) => {
        控制台日志(问候语);
    });
getUserName()
    .then((name: string) => {
        return `Hi ${name}!`;       1
    })
    .then((greeting: string) => {
        console.log(greeting);
    });

  • 1 在这种情况下,我们不返回承诺,但 then() 将其转换为 Promise<string>。
  • 1 In this case, we don’t return a promise, but then() converts this to a Promise<string>.

这在直觉上应该是有意义的:即使我们的延续只是返回一个字符串,因为它被链接到一个承诺,它不能立即执行。这一事实自动使它在最初的承诺得到解决时成为一个承诺。

This should make sense intuitively: even if our continuation just returns a string, because it is chained to a promise, it can’t execute right away. That fact automatically makes it a promise to be settled when the original promise is settled.

编写 promise 的其他方式

到目前为止,我们已经研究了then()(and catch()),哪个链式承诺在一起,以便它们一个接一个地结算。还有更多方法可以安排异步函数的执行:通过Promise.all()Promise.race()。这些是类上提供的静态方法PromisePromise.all()将一组承诺作为参数,并返回一个承诺,该承诺在所有提供的承诺都已结算时结算。Promise.race()接受一组承诺并返回一个承诺,该承诺在任何一个承诺得到解决时得到解决。

So far, we’ve looked at then() (and catch()), which chain promises together so that they settle one after the other. There are a couple more ways to schedule the execution of asynchronous functions: via Promise.all() and Promise.race(). These are static methods provided on the Promise class. Promise.all() takes as arguments a set of promises and returns a promise that is settled when all the provided promises are settled. Promise.race() takes a set of promises and returns a promise that is settled when any one of the promises is settled.

当我们想要调度一组独立的异步函数时,我们可以使用Promise.all(),例如从数据库中获取用户收件箱消息和从 CDN 中获取他们的个人资料图片,然后将这两个值传递给 UI,如清单 6.21所示。我们不想一个接一个地对这些获取函数进行排序,因为它们并不相互依赖。另一方面,我们确实想收集它们的结果并将它们传递给另一个函数。

We can use Promise.all() when we want to schedule a set of independent asynchronous functions, such as fetching user inbox messages from a database and their profile picture from a CDN, and then passing both values to the UI, as shown in listing 6.21. We don’t want to sequence these fetching functions one after another, because they don’t depend on one another. On the other hand, we do want to gather their results and pass them to another function.

清单 6.21。使用Promise.all()顺序执行
类 Inb​​oxMessage { /* ... */ }
类 ProfilePicture { /* ... */ }

声明函数 getInboxMessages(): Promise<InboxMessage[]>;      1
声明函数 getProfilePicture(): Promise<ProfilePicture>;     1个

声明函数 renderUI(                                          2
    消息:InboxMessage[],图片:ProfilePicture:无效;

Promise.all([getInboxMessages(), getProfilePicture()])              3 
    .then((values: [InboxMessage[], ProfilePicture]) => {           4 
        renderUI(values[0], values[1]);                             5 
    });
class InboxMessage { /* ... */ }
class ProfilePicture { /* ... */ }

declare function getInboxMessages(): Promise<InboxMessage[]>;      1
declare function getProfilePicture(): Promise<ProfilePicture>;     1

declare function renderUI(                                         2
    messages: InboxMessage[], picture: ProfilePicture): void;

Promise.all([getInboxMessages(), getProfilePicture()])             3
    .then((values: [InboxMessage[], ProfilePicture]) => {          4
        renderUI(values[0], values[1]);                            5
    });

  • 1 getInboxMessages() 和getProfilePicture() 是独立的异步函数。
  • 1 getInboxMessages() and getProfilePicture() are independent asynchronous functions.
  • 2 renderUI() 需要两个函数的结果。
  • 2 renderUI() needs the result from both functions.
  • 3 Promise.all() 创建一个承诺,当两个函数都解决了他们的承诺时。
  • 3 Promise.all() creates a promise settled when both functions resolve their promises.
  • 4 values 是一个包含两个结果的元组。
  • 4 values is a tuple containing both results.
  • 5 我们将检索到的值传递给 renderUI()。
  • 5 We pass the values retrieved to renderUI().

像这样的模式将很难通过回调实现,因为没有加入回调的机制。

A pattern like this would be significantly harder to achieve with callbacks, as there is no mechanism to join them.

Promise.race()让我们看一下在下一个清单中使用的示例。假设跨两个节点复制用户配置文件。我们尝试从两者中获取它,以最快的为准。在这种情况下,只要我们从任何一个节点获得结果,我们就可以继续。

Let’s look at an example of using Promise.race() in the next listing. Suppose that the user profile is replicated across two nodes. We try to fetch it from both, and whichever is the fastest wins. In this case, as soon as we get a result from any one of the nodes, we can proceed.

清单 6.22。使用Promise.race()顺序执行
类 UserProfile { /* ... */ }

声明函数 getProfile(node: string): Promise<UserProfile>;

声明函数 renderUI(profile: UserProfile): void;

Promise.race([getProfile("node1"), getProfile("node2")])     1 
    .then((profile: UserProfile) => {                        2
        renderUI(配置文件);
    });
class UserProfile { /* ... */ }

declare function getProfile(node: string): Promise<UserProfile>;

declare function renderUI(profile: UserProfile): void;

Promise.race([getProfile("node1"), getProfile("node2")])    1
    .then((profile: UserProfile) => {                       2
        renderUI(profile);
    });

  • 1 我们为每个节点调用一次 getProfile()。
  • 1 We call getProfile() once for each of the nodes.
  • 2 在本例中,continuation 的参数是单个 UserProfile——赢得比赛的那个。
  • 2 The argument to the continuation is a single UserProfile in this case—the one that won the race.

如果使用没有承诺的回调,这种情况将更难实现(图 6.11)。

This scenario would be more difficult to achieve by using callbacks without promises (figure 6.11).

图 6.11。组合承诺的不同方式。然后: 安顿下来Promise 1并交给; 安顿下来并递给。全部: , , 并结算。当所有这些都已解决时,获取它们的所有值并可以继续,解决自己的价值。种族:其中一个承诺首先解决(在本例中为)。得到并可以继续,确定自己的价值。 Value 1Promise 2Promise 2Value 2Promise 3 Promise 123Promise.allPromise 2Promise.raceValue 2

Promises 为运行异步函数提供了一个清晰的抽象。它们不仅使代码比通过then()andcatch()方法使用回调更具可读性,这可以实现排序,而且还可以处理错误传播以及通过Promise.all()and加入或竞争多个承诺Promise.race()加入或竞争多个承诺。大多数主流编程语言都提供 Promise 库,它们都提供类似的功能,即使方法的名称略有不同。(race()有时称为any(),例如。)

Promises provide a clean abstraction for running asynchronous functions. They not only make code more readable than using callbacks through the then() and catch() methods, which enable sequencing, but also handle error propagation and joining or racing multiple promises via Promise.all() and Promise.race(). Promise libraries are available in most mainstream programming languages, and they all provide similar functionality, even if the name of the methods is slightly different. (race() is sometimes called any(), for example.)

这是库在帮助我们编写干净的异步代码方面所能达到的极限。使异步代码更具可读性需要更新语言本身的语法。就像yield语句让我们更轻松地表达生成器函数一样,许多语言扩展了它们的语法asyncawait使我们能够更轻松地编写异步函数。

This is about as far as libraries can go in helping us write clean asynchronous code. Making asynchronous code more readable requires updates to the syntax of the language itself. Much as a yield statement allows us to more easily express a generator function, many languages extended their syntax with async and await to enable us to write asynchronous functions more easily.

6.4.4.异步/等待

6.4.4. async/await

使用 promises,我们提示我们的用户提供各种信息,使用 continuations 对问题进行排序。让我们再看看下一个清单中的实现。我们要把它包装成一个getUserData()函数。

Using promises, we prompted our user for various pieces of information, using continuations to sequence the questions. Let’s take another look at that implementation in the next listing. We’re going to wrap it into a getUserData() function.

清单 6.23。链接承诺审查
函数 getUserData(): void {
    获取用户名()
        .then((名称: 字符串) => {
            console.log(`嗨 ${name}!`);
            返回 getUserBirthday(名字);
        })
        .then((生日: 日期) => {
            今天常量:Date = new Date();
            如果(生日.getMonth()==今天.getMonth()&&
                生日.getDay() == 今天.getDay())
                console.log('生日快乐!');
            返回 getUserEmail(生日);
        })
        .then((email: string) => {
            /* ... */
        });
}
function getUserData(): void {
    getUserName()
        .then((name: string) => {
            console.log(`Hi ${name}!`);
            return getUserBirthday(name);
        })
        .then((birthday: Date) => {
            const today: Date = new Date();
            if (birthday.getMonth() == today.getMonth() &&
                birthday.getDay() == today.getDay())
                console.log('Happy birthday!');
            return getUserEmail(birthday);
        })
        .then((email: string) => {
            /* ... */
        });
}

再次注意,每个延续都将一个值作为参数,该值的类型与前一个函数的承诺类型相同。async/await允许我们在代码中更好地表达这一点。我们可以将生成器和*/yield我们在上一节中讨论的语法进行比较。

Notice again that each continuation takes as argument a value of the same type as the type of the promise from the preceding function. async/await allows us to express this better in code. We can draw a parallel with generators and the */yield syntax we discussed in a previous section.

async是出现在关键字之前的关键字function,就像在生成器中*出现在关键字之后一样function。与*can only used only if the function returns an相同Iteratorasync只能出现在返回 a 的函数中Promise,就像*,async不会改变函数的类型一样。function getUserData(): Promise<string>并且async function getUserData(): Promise<string>具有相同的类型:() => Promise<string>. *将函数标记为生成器并允许我们yield在其中调用的方式与async将函数标记为异步函数并允许我们await在其中 调用的方式相同。

async is a keyword that comes before the keyword function, much as the * appears after the keyword function in generators. In the same way that * can be used only if the function returns an Iterator, async can appear only in a function that returns a Promise, just as *, async does not change the type of the function. function getUserData(): Promise<string> and async function getUserData(): Promise<string> have the same type: () => Promise<string>. The same way that * marks a function as a generator and allows us to call yield inside it, async marks a function as asynchronous and allows us to call await inside it.

我们可以await在返回承诺的函数之前使用,以获取该承诺结算时返回的值。我们不写作getUserName().then -((name: string) => { /* ... */ }),而是写作let name: string = await getUser-Name()。在了解它是如何工作的之前,让我们看看我们将如何getUserData()使用asyncand编写await

We can use await before a function that returns a promise to get the value returned when that promise settles. Instead of writing getUserName().then -((name: string) => { /* ... */ }), we write let name: string = await getUser-Name(). Before walking through how this works, let’s look at how we would write getUserData() with async and await.

清单 6.24。使用async/await
async function getUserData(): Promise<void> {            1 
    let name: string = await getUserName();             2 
    console.log(`嗨 ${name}!`);                         3个

    让生日:Date = await getUserBirthday(name);   4个
    今天常量:Date = new Date();
    如果(生日.getMonth()==今天.getMonth()&&
        生日.getDay() == 今天.getDay())
        console.log('生日快乐!');

    let email: string = await getUserEmail(birthday);   5个
    /* ... */
}
async function getUserData(): Promise<void> {           1
    let name: string = await getUserName();             2
    console.log(`Hi ${name}!`);                         3

    let birthday: Date = await getUserBirthday(name);   4
    const today: Date = new Date();
    if (birthday.getMonth() == today.getMonth() &&
        birthday.getDay() == today.getDay())
        console.log('Happy birthday!');

    let email: string = await getUserEmail(birthday);   5
    /* ... */
}

  • 1 getUserData() 必须返回一个 Promise,因为它被标记为异步。
  • 1 getUserData() must return a Promise because it is marked as async.
  • 2 我们等待 getUserName() 解决并给我们一个名称字符串。
  • 2 We await getUserName() to settle and give us a name string.
  • 3 我们可以在同一个函数中使用这个名称字符串。
  • 3 We can use this name string in this same function.
  • 4 我们等待 getUserBirthday() 来解决并给我们一个生日。
  • 4 We await getUserBirthday() to settle and give us a birthday.
  • 5 getUserEmail()也是如此;我们等待已解决的承诺并获得字符串值。
  • 5 The same is true for getUserEmail(); we await the settled promise and get the string value.

我们立即看到,getUserData()以这种方式编写我们的代码比将 promise 与then(). 编译器生成相同的代码;引擎盖下没有什么特别的。这种技术只是一种表达延续链的更好方式。then()我们可以将所有代码写在一个函数中,而不是将每个延续放在一个单独的函数中并通过 连接它们,每当我们调用另一个返回承诺的函数时,我们await就会返回它的结果。

We immediately see that writing our getUserData() this way makes it even more readable than chaining promises with then(). The compiler generates the same code; there is nothing special under the hood. This technique is simply a nicer way to express a chain of continuations. Instead of putting each continuation in a separate function and connecting them via then(), we can write all the code in a single function, and whenever we call another function that returns a promise, we await its result.

每个都await相当于在它之后获取代码并将其放在一个then()延续中:这减少了我们需要编写的 lambda 的数量,并使异步代码像同步代码一样读取。至于catch(),如果没有返回值,可能是我们遇到了异常,异常从调用中抛出,可以用正则/语句await捕获。只需将调用包装在一个块中即可捕获预期的错误。 trycatchawaittry

Each await is the equivalent of taking the code after it and placing it in a then() continuation: this reduces the number of lambdas we need to write and makes asynchronous code read just like synchronous code. As for catch(), if there is no value to return, perhaps because we encountered an exception, the exception is thrown from the await call and can be caught with a regular try/catch statement. Simply wrap the await call in a try block to catch the expected errors.

6.4.5.清理异步代码回顾

6.4.5. Clean asynchronous code recap

让我们快速回顾一下本节介绍的编写异步代码的方法。我们从回调开始,将回调函数传递给一个异步函数,该函数在其工作完成时调用它。这种方法可行,但我们通常会在回调中出现大量嵌套回调,这使得代码更难理解。如果我们需要所有异步函数的结果才能继续,那么加入几个独立的异步函数也是非常困难的。

Let’s quickly review the approaches to writing asynchronous code that we covered in this section. We started with callbacks, passing a callback function to an asynchronous function that calls it when its work is done. This approach works, but we’ll usually end up with a lot of nested callbacks within callbacks, which makes code harder to follow. It’s also very difficult to join several independent asynchronous functions if we need the results from all of them to proceed.

接下来,我们看看承诺。Promises 为编写异步代码提供了抽象。它们处理代码执行的调度(在依赖线程的语言中,它们是在线程上调度的)并为我们提供了一种方法来提供称为延续的函数,当承诺被解决(具有价值)或被拒绝(遇到错误)。Promise.all()Promises 还提供了通过和加入和竞争一组 promise 的方法Promise.race()

Next, we looked at promises. Promises provide an abstraction for writing asynchronous code. They handle scheduling the execution of the code (in languages that rely on threads, they get scheduled on threads) and provide a way for us to provide functions called continuations, which get called when the promise is settled (has a value) or rejected (encountered an error). Promises also provide ways to join and race a set of promises via Promise.all() and Promise.race().

最后,async/await语法现在在大多数主流编程语言中都很常见,它提供了一种更简洁的方式来编写读取起来就像常规代码一样的异步代码。then()我们不是提供一个延续,而是await一个承诺的结果,并从那里继续。计算机执行的底层代码是相同的,但语法更易读。

Finally, async/await syntax, now common in most mainstream programming languages, provides an even-cleaner way to write asynchronous code that reads just like regular code. Instead of providing a continuation with then(), we await the result of a promise and continue from there. The underlying code executed by the computer is the same, but the syntax is much nicer to read.

6.4.6.练习

6.4.6. Exercises

1个

承诺从哪个状态开始?

  1. 入驻
  2. 拒绝
  3. 待办的
  4. 任何

1

Which state does a promise start in?

  1. Settled
  2. Rejected
  3. Pending
  4. Any

2个

当 promise 被拒绝时,以下哪个链将被调用?

  1. then()
  2. catch()
  3. all()
  4. race()

2

Which of the following chains a continuation to be called when the promise is rejected?

  1. then()
  2. catch()
  3. all()
  4. race()

3个

当一整套 promises 被解决时,以下哪个链将被调用?

  1. then()
  2. catch()
  3. all()
  4. race()

3

Which of the following chains a continuation to be called when a whole set of promises is settled?

  1. then()
  2. catch()
  3. all()
  4. race()

概括

Summary

  • 闭包是一个 lambda,它还保留来自其周围函数的一个状态。
  • A closure is a lambda that also holds on to a piece of state from its surrounding function.
  • 我们可以通过使用闭包并捕获装饰函数来实现更简单的装饰器模式,而不是实现一个全新的类型。
  • We can implement a simpler decorator pattern by using a closure and capturing the decorated function instead of implementing a whole new type.
  • 我们可以通过使用跟踪计数器状态的闭包来实现计数器。
  • We can implement a counter by using a closure that tracks the counter state.
  • 使用 */yield 语法编写的生成器是可恢复的函数。
  • A generator, written using */yield syntax, is a resumable function.
  • 长时间运行的操作应该异步运行,这样它们就不会阻塞程序的其余部分。
  • Long-running operations should run asynchronously so that they don’t block the rest of the program.
  • 异步执行的两个主要模型是线程和事件循环。
  • The two main models for asynchronous execution are threads and event loops.
  • 回调是传递给异步函数的函数,在异步函数完成时调用该函数。
  • A callback is a function passed to an asynchronous function that is invoked when the asynchronous function completes.
  • Promises 为运行异步函数提供了一个通用的抽象,并提供了延续作为回调的替代方法。承诺可以是待定的、已结算的(获得的值)或拒绝的(遇到错误)。
  • Promises provide a common abstraction for running asynchronous functions and provide continuations as an alternative to callbacks. A promise can be pending, settled (value obtained), or rejected (error encountered).
  • Promise.all()并且Promise.race()是加入和竞速一系列承诺的机制。
  • Promise.all() and Promise.race() are mechanisms for joining and racing a set of promises.
  • async/await是编写基于承诺的代码的现代语法,就好像它是同步代码一样。
  • async/await is modern syntax for writing promise-based code as though it were synchronous code.

现在我们已经深入介绍了函数类型的应用,从将函数作为参数传递到生成器和异步函数的基础知识,我们将继续讨论下一个主要主题:子类型。正如我们将在第 7 章中看到的,子类型比继承要多得多。

Now that we’ve covered applications of function types in depth, from the basics of passing functions as arguments all the way to generators and asynchronous functions, we’ll move on to the next major topic: subtypes. As we’ll see in chapter 7, there is a lot more to subtypes than inheritance.

习题答案

Answers to exercises

一个简单的装饰器模式

A simple decorator pattern

1个

一个可能的实现返回一个将日志记录添加到包装工厂的函数:

函数 loggingDecorator(工厂:()=> 小部件):()=> 小部件 {
    返回()=> {
        console.log("Widget 已创建");
        返回工厂();
    }
}

1

A possible implementation returning a function that adds logging to the wrapped factory:

function loggingDecorator(factory: () => Widget): () => Widget {
    return () => {
        console.log("Widget created");
        return factory();
    }
}

 

 

实现一个计数器

Implementing a counter

2个

使用从包装函数 捕获a和捕获的闭包的可能实现:b

函数 fib(): () => 数字 {
    让一个:数字= 0;
    让 b: 数字 = 1;

    返回()=> {
        让下一个:数字=一个;
        一 = b;
        b = b + 下一个;
        接下来返回;
    }
}

2

A possible implementation using a closure that captures a and b from the wrapping function:

function fib(): () => number {
    let a: number = 0;
    let b: number = 1;

    return () => {
        let next: number = a;
        a = b;
        b = b + next;
        return next;
    }
}

3个

使用生成序列中下一个数字的生成器的可能实现:

函数 *fib2(): IterableIterator<number> {
    让一个:数字= 0;
    让 b: 数字 = 1;

    而(真){
        让下一个:数字=一个;
        一 = b;
        b = a + 下一个;
        接下来屈服;
    }
}

3

A possible implementation using a generator that yields the next number in the sequence:

function *fib2(): IterableIterator<number> {
    let a: number = 0;
    let b: number = 1;

    while (true) {
        let next: number = a;
        a = b;
        b = a + next;
        yield next;
    }
}

 

 

异步执行长时间运行的操作

Executing long-running operations asynchronously

1个

d—线程和事件循环都可以用来实现异步执行。

1

d—Both threads and an event loop can be used to implement asynchronous execution.

2个

b—事件循环不并行执行代码。它可以异步排队和执行功能,但不能同时进行。

2

b—An event loop does not execute code in parallel. It can queue and execute functions asynchronously, but not at the same time.

3个

a——线程允许并行执行;多个线程可以同时运行多个函数。

3

a—Threads allow parallel execution; multiple threads can run multiple functions at the same time.

 

 

简化异步代码

Simplifying asynchronous code

1个

c—一个 promise 从 pending 状态开始。

1

c—A promise starts in the pending state.

2个

c—我们用来catch()链接一个在承诺被拒绝时被调用的延续。

2

c—We use catch() to chain a continuation that gets called when a promise is rejected.

3个

c—我们用来all()链接一个延续,当所有的承诺都被解决时,它会被调用。

3

c—We use all() to chain a continuation that gets called when all promises are settled.

 

 

第7章。分型

Chapter 7. Subtyping

本章涵盖

This chapter covers

  • 在 TypeScript 中消除类型歧义
  • Disambiguating types in TypeScript
  • 安全反序列化
  • Safe deserialization
  • 错误案例的值
  • Values for error cases
  • 求和类型、集合和函数的类型兼容性
  • Type compatibility for sum types, collections, and functions

现在我们已经介绍了基本类型、组合和函数类型,是时候看看类型系统的另一个方面了:类型之间的关系。在本章中,我们将介绍子类型关系。尽管您可能从面向对象编程中熟悉它,但我们不会在本章中介绍继承。相反,我们将专注于一组不同的子类型应用。

Now that we’ve covered primitive types, composition, and function types, it’s time to look at another aspect of type systems: relationships between types. In this chapter, we’ll introduce the subtyping relationship. Although you may be familiar with it from object-oriented programming, we will not cover inheritance in this chapter. Instead, we will focus on a different set of applications of subtyping.

首先,我们讨论什么是子类型化以及编程语言实现它的两种方式:结构化和名义化。然后我们将重新审视我们的火星气候轨道器示例,并解释unique symbol我们在第 4 章讨论类型安全时使用的技巧。

First, we’ll talk about what subtyping is and the two ways in which programming languages implement it: structural and nominal. Then we will revisit our Mars Climate Orbiter example and explain the unique symbol trick we used in chapter 4 when discussing type safety.

因为一个类型可以是另一个类型的子类型,它也可以有其他子类型,我们将看看这个类型层次结构:我们通常有一个类型位于这个层次结构的顶部,有时,一个类型位于这个层次结构的顶部底部。我们将看到如何在诸如反序列化之类的场景中使用这种顶级类型,在这种场景中我们没有大量可用的类型信息。我们还将看到如何使用底部类型作为错误情况的值。

Because a type can be a subtype of another type, and it can also have other subtypes, we will look at this type hierarchy: we usually have a type that sits at the top of this hierarchy and, sometimes, a type that sits at the bottom. We’ll see how we can use this top type in a scenario such as deserialization, in which we don’t have a lot of typing information readily available. We’ll also see how to use a bottom type as a value for error cases.

在本章的后半部分,我们将研究更复杂的子类型关系是如何建立的。这有助于我们了解哪些价值观可以替代哪些其他价值观。我们是否需要实现包装器,或者我们可以简单地按原样传递另一种类型的值吗?如果一种类型是另一种类型的子类型,那么这两种类型的集合之间的子类型关系是什么?接受或返回这些类型的参数的函数呢?我们将举一个涉及形状的简单示例,看看我们如何将它们作为求和类型、集合和函数传递,这个过程也称为方差。我们还将了解不同类型的方差。但首先,让我们看看子类型在 TypeScript 中的含义。

In the second half of the chapter, we will look at how more-complex subtyping relationships are established. This helps us understand what values we can substitute for what other values. Do we need to implement wrappers, or can we simply pass a value of another type as is? If a type is a subtype of another type, what is the subtyping relationship between collections of those two types? What about functions that take or return arguments of these types? We’ll take a simple example involving shapes and see how we can pass them around as sum types, collections, and functions, a process also known as variance. We’ll also learn about the different types of variance. But first, let’s see what subtyping means in TypeScript.

7.1. 区分 TypeScript 中的相似类型

7.1. Distinguishing between similar types in TypeScript

本书中的大多数示例,即使是用 TypeScript 呈现的,也是与语言无关的,可以翻译成大多数其他主流编程语言。本节例外;我们将讨论一种特定于 TypeScript 的技术。我们这样做是因为它是讨论子类型的一个很好的转折点。

Most of the examples in this book, even though presented in TypeScript, are language-agnostic and can be translated for most other mainstream programming languages. This section is an exception; we’ll discuss a technique specific to TypeScript. We’ll do this because it’s a great segue into a discussion of subtyping.

让我们重新审视第 4 章中的磅力秒/牛顿秒示例。请记住,我们将两个不同的测量单位建模为两个不同的类别。我们想确保类型检查器不允许我们将一种类型的值误解为另一种类型,因此我们过去常常unique symbol消除它们的歧义。我们没有详细说明为什么我们当时必须这样做,但现在让我们在下面的清单中这样做。

Let’s revisit the pound-force second/Newton-second example from chapter 4. Remember that we were modeling two different units of measurements as two different classes. We wanted to make sure that the type checker wouldn’t allow us to misinterpret a value of one type as the other, so we used unique symbol to disambiguate them. We didn’t go into the details of why we had to do this then, but let’s do it now in the following listing.

清单 7.1。磅力秒和牛顿秒类型
声明 const NsType:唯一符号;      1个

类 Ns {
    值:数字;
    [NsType] : void;                       1个

    构造函数(值:数字){
        this.value = 值;
    }
}

声明 const LbfsType:唯一符号;    2个

类 Lbfs {
    值:数字;
    [LbfsType] : void;                     2个

    构造函数(值:数字){
        this.value = 值;
    }
}
declare const NsType: unique symbol;      1

class Ns {
    value: number;
    [NsType]: void;                       1

    constructor(value: number) {
        this.value = value;
    }
}

declare const LbfsType: unique symbol;    2

class Lbfs {
    value: number;
    [LbfsType]: void;                     2

    constructor(value: number) {
        this.value = value;
    }
}

  • 1 我们将 NsType 声明为唯一符号,并为 Ns 添加一个类型为 void 的名为 [NsType] 的属性。
  • 1 We declare NsType as a unique symbol and add a property named [NsType] of type void to Ns.
  • 2 我们还将 LbfsType 声明为唯一符号,并向 Lbfs 添加类型为 void 的 [LbfsType] 属性。
  • 2 We also declare a LbfsType as a unique symbol and add a [LbfsType] property of type void to Lbfs.

如果我们省略这两个声明,就会发生一件有趣的事情:我们可以将一个Ns对象作为一个Lbfs对象传递,反之亦然,而不会从编译器中得到任何错误。让我们实现一个函数来演示这个过程:一个名为的函数acceptNs()需要一个Ns参数。然后我们将尝试在下一个清单 Lbfs中将一个对象传递给。acceptNs()

If we omit these two declarations, an interesting thing happens: we can pass a Ns object as a Lbfs object, and vice versa, without getting any errors from the compiler. Let’s implement a function to demonstrate this process: a function named acceptNs() that expects a Ns argument. Then we’ll try to pass a Lbfs object to acceptNs() in the next listing.

清单 7.2。没有唯一符号的磅力秒和牛顿秒
类 Ns {                                         1
    值:数字;

    构造函数(值:数字){
        this.value = 值;
    }
}

Lbfs 类 {                                       1
    值:数字;

    构造函数(值:数字){
        this.value = 值;
    }
}

函数 acceptNs(momentum: Ns): void {            2
    console.log(`Momentum: ${momentum.value} Ns`);
}

acceptNs(新 Lbfs(10));                           3个
class Ns {                                        1
    value: number;

    constructor(value: number) {
        this.value = value;
    }
}

class Lbfs {                                      1
    value: number;

    constructor(value: number) {
        this.value = value;
    }
}

function acceptNs(momentum: Ns): void {           2
    console.log(`Momentum: ${momentum.value} Ns`);
}

acceptNs(new Lbfs(10));                           3

  • 1 Ns 和 Lbfs 不再具有唯一的符号属性。
  • 1 Ns and Lbfs no longer have a unique symbol property.
  • 2 acceptNs() 将 Ns 对象作为参数并记录其值。
  • 2 acceptNs() takes a Ns object as an argument and logs its value.
  • 3 我们将 Lbfs 实例传递给 acceptNs()。
  • 3 We pass a Lbfs instance to acceptNs().

令人惊讶的是,这段代码可以运行并记录Momentum: 10 Ns.,这绝对不是我们想要的。我们定义这两种不同类型的原因是为了避免混淆这两种测量单位和使火星气候轨道器坠毁。这是怎么回事?要了解发生了什么,我们需要了解子类型。

Surprisingly, this code works and logs Momentum: 10 Ns., which is definitely not what we want. The reason why we defined these two separate types was to avoid confusing the two units of measure and crashing the Mars Climate Orbiter. What’s going on? To understand what is happening, we need to understand subtyping.

分型

如果一个类型的实例可以安全地用于任何需要 一个实例的地方,那么这个类型S就是一个类型的子类型。TST

A type S is a subtype of a type T if an instance of S can be safely used anywhere an instance of T is expected.

这是著名的Liskov 替换原则的非正式定义。如果我们可以在需要超类型实例时使用子类型实例而无需更改代码,则两种类型处于子类型-超类型关系中。

This is an informal definition of the famous Liskov substitution principle. Two types are in a subtype-supertype relationship if we can use an instance of the subtype whenever an instance of the supertype is expected without having to change the code.

建立子类型关系有两种方式。大多数主流编程语言(如 Java 和 C#)使用的第一个称为名义子类型化。在名义子类型化中,如果我们使用像class Triangle extends Shape. Triangle现在我们可以在需要实例的时候使用实例Shape(例如函数的参数)。如果我们不声明Triangle为 extending Shape,编译器将不允许我们将它用作Shape.

There are two ways in which subtyping relationships are established. The first one, which most mainstream programming languages (such as Java and C#) use, is called nominal subtyping. In nominal subtyping, a type is the subtype of another type if we explicitly declare it as such, using syntax like class Triangle extends Shape. Now we can use an instance of Triangle whenever an instance of Shape is expected (such as as argument to a function). If we don’t declare Triangle as extending Shape, the compiler won’t allow us to use it as a Shape.

另一方面,结构子类型化不需要我们在代码中明确声明子类型化关系。可以使用类型的实例(例如Lbfs)代替另一种类型(例如 )Ns,只要它具有其他类型声明的所有成员即可。换句话说,如果一个类型与另一个类型具有相似的结构(相同的成员和可选的附加成员),它会自动被认为是另一个类型的子类型。

On the other hand, structural subtyping doesn’t require us to state the subtyping relationship explicitly in code. An instance of a type, such as Lbfs, can be used instead of another type, such as Ns, as long as it has all the members that the other type declares. In other words, if a type has a similar structure to another type (the same members and optionally additional members), it is automatically considered to be a subtype of that other type.

名义和结构子类型化

在名义子类型化中,如果我们明确声明一个类型,则该类型是另一种类型的子类型。在结构子类型化中,如果一个类型具有超类型的所有成员和可选的附加成员,则该类型是另一种类型的子类型。

In nominal subtyping, a type is a subtype of another type if we explicitly declare it as such. In structural subtyping, a type is a subtype of another type if it has all the members of the supertype and, optionally, additional members.

与 C# 和 Java 不同,TypeScript 使用结构子类型化。这就是为什么如果我们将Ns和声明为只有type 成员的Lbfs类,它们仍然可以互换使用。 valuenumber

Unlike C# and Java, TypeScript uses structural subtyping. That’s the reason why, if we declare Ns and Lbfs as classes with only a value member of type number, they can still be used interchangeably.

7.1.1.结构和名义子类型的优缺点

7.1.1. Structural and nominal subtyping pros and cons

在许多情况下,结构子类型化很有用,因为它允许我们在类型之间建立关系,即使它们不在我们的控制之下。假设我们使用的库将User类型定义为具有nameage。在我们的代码中,我们有一个Named接口需要一个name实现类型的属性。我们可以在需要Usera 的任何时候使用一个实例Named,即使User没有显式实现Named,如下一个清单所示。(我们没有声明class User implements Named。)

In many cases, structural subtyping is useful, as it allows us to establish relationships between types even if they are not under our control. Suppose that a library we use defines a User type as having a name and age. In our code, we have a Named interface that requires a name property on implementing types. We can use an instance of User whenever a Named is expected, even though User does not explicitly implement Named, as shown in the next listing. (We don’t have the declaration class User implements Named.)

清单 7.3。User在结构上是Named
/* 库代码 */
用户类 {                              1
    名称:字符串;
    年龄:数字;

    构造函数(名称:字符串,年龄:数字){
        this.name = 名称;
        这个。年龄=年龄;
    }
}

/* 我们的代码 */
接口命名为{
    名称:字符串;
}

功能问候(命名:命名):void {      2
    console.log(`嗨 ${named.name}!`);
}

问候(新用户(“爱丽丝”,25));            3个
/* Library code */
class User {                             1
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

/* Our code */
interface Named {
    name: string;
}

function greet(named: Named): void {     2
    console.log(`Hi ${named.name}!`);
}

greet(new User("Alice", 25));            3

  • 1 User 是我们无法修改的来自外部库的类型。
  • 1 User is a type from an external library that we can’t modify.
  • 2 greet() 需要一个符合 Named 接口的实例。
  • 2 greet() expects an instance conforming to the Named interface.
  • 3 我们可以将 User 实例作为 Named 传递。
  • 3 We can pass a User instance as a Named.

如果我们必须显式声明Userimplements Named,我们就会遇到麻烦,因为User它是一个来自外部库的类型。我们不能更改库代码,因此我们必须通过声明一个扩展User和实现Named( class NamedUser extends User implements Named {}) 的新类型来解决这种情况,只是为了连接这两种类型。如果我们的类型系统使用结构子类型,我们不需要这样做。

If we had to explicitly declare that User implements Named, we would be in trouble, because User is a type that comes from an external library. We can’t change library code, so we would have to work around this situation by declaring a new type that extends User and implements Named (class NamedUser extends User implements Named {}) just to connect the two types. We don’t need to do this if our type system uses structural subtyping.

另一方面,在某些情况下,我们绝对不希望一个类型仅仅基于其结构就被认为是另一个类型的子类型。例如,Lbfs绝不能使用实例代替实例。Ns在名义子类型中,这是默认值,这使得避免错误变得非常容易。另一方面,结构子类型要求我们做更多的工作来确保一个值是我们期望的类型,而不是具有相似形状的类型的值。在这种情况下,结构子类型化要好得多。

On the other hand, in some situations we absolutely don’t want a type to be considered a subtype of another type based simply on its structure. A Lbfs instance should never be used instead of a Ns instance, for example. In nominal subtyping, this is the default, which makes it very easy to avoid mistakes. On the other hand, structural subtyping requires us to do more work to ensure that a value is of the type we expect it to be rather than a value of a type with a similar shape. In such scenarios, structural subtyping is much better.

如果我们想使用名义子类型化,我们可以使用多种技术在 TypeScript 中强制执行它。其中之一就是unique symbol我们在整本书中使用的技巧。让我们放大它。

If we want to use nominal subtyping, we can use several techniques to enforce it in TypeScript. One of them is the unique symbol trick we’ve used throughout the book. Let’s zoom in on it.

7.1.2.在 TypeScript 中模拟名义子类型化

7.1.2. Simulating nominal subtyping in TypeScript

在我们的Ns/Lbfs案例中,我们正在有效地尝试模拟名义子类型化。我们希望确保编译器Ns仅在我们显式声明一个类型时才将其视为子类型,而不仅仅是因为它有一个value成员。

In our Ns/Lbfs case, we are effectively trying to simulate nominal subtyping. We want to make sure that the compiler considers a type to be a subtype of Ns only if we explicitly declare it as such, not just because it has a value member.

为此,我们需要向其中添加一个Ns其他类型无法意外声明的成员。在 TypeScript 中,unique symbol生成保证在所有代码中唯一的“名称”。不同的unique symbol声明将生成不同的名称,并且用户声明的名称永远不会与生成的名称相匹配。

To achieve this, we need to add a member to Ns that no other type can declare accidentally. In TypeScript, unique symbol generates a “name” that’s guaranteed to be unique across all the code. Different unique symbol declarations will generate different names, and no user-declared name can ever match a generated name.

我们声明一个唯一的符号来表示我们的Ns类型为NsType. 唯一符号声明如下所示:(declare const NsType: unique symbol清单 7.1 所示)。现在我们有了一个唯一的名称,我们可以通过将名称放在方括号中来创建具有该名称的属性。我们需要为这个属性定义一个类型,但我们实际上并不打算给它分配任何东西,因为我们只是用它来消除类型歧义。因为我们不关心它的实际值,单位类型最适合这个目的,所以我们使用void.

We declare a unique symbol to represent our Ns type as NsType. The unique symbol declaration looks like this: declare const NsType: unique symbol (as in listing 7.1). Now that we have a unique name, we can create a property with that name by putting the name in square brackets. We need to define a type for this property, but we aren’t really going to assign anything to it because we’re just using it to disambiguate types. Because we don’t care about its actual value, a unit type is best suited for this purpose, so we use void.

我们对 做同样的事情Lbfs,现在类型有不同的结构:一个有一个[NsType]属性,另一个有一个[LbfsType]属性,如图清单 7.4中。因为我们使用了unique symbol,所以不可能不小心在另一个类型上定义同名的属性。Ns为和now提出子类型的唯一方法Lbfs是显式继承它们。

We do the same for Lbfs, and now the types have different structures: one of them has a [NsType] property, and the other has a [LbfsType] property, as shown in listing 7.4. Because we used unique symbol, it’s impossible to accidentally define a property with the same name on another type. The only way to come up with a subtype for Ns and Lbfs now is to explicitly inherit from them.

清单 7.4。模拟名义子类型
声明 const NsType:唯一符号;

类 Ns {
    值:数字;
    [NsType]: void;

    构造函数(值:数字){
        this.value = 值;
    }
}

声明 const LbfsType:唯一符号;

类 Lbfs {
    值:数字;
    [LbfsType]: void;

    构造函数(值:数字){
        this.value = 值;
    }
}

函数 acceptNs(momentum: Ns): void {
    console.log(`Momentum: ${momentum.value} Ns`);
}

acceptNs(新 Lbfs(10));        1个
declare const NsType: unique symbol;

class Ns {
    value: number;
    [NsType]: void;

    constructor(value: number) {
        this.value = value;
    }
}

declare const LbfsType: unique symbol;

class Lbfs {
    value: number;
    [LbfsType]: void;

    constructor(value: number) {
        this.value = value;
    }
}

function acceptNs(momentum: Ns): void {
    console.log(`Momentum: ${momentum.value} Ns`);
}

acceptNs(new Lbfs(10));        1

  • 1 这不再编译。
  • 1 This no longer compiles.

当我们尝试将Lbfs实例作为 a传递时Ns,我们会收到以下错误:

When we try to pass a Lbfs instance as a Ns, we get the following error:

“Lbfs”类型的参数不可分配给参数
输入“Ns”。“Lbfs”类型中缺少属性“[NsType]”
但在类型“Ns”中是必需的。
Argument of type 'Lbfs' is not assignable to parameter of
type 'Ns'. Property '[NsType]' is missing in type 'Lbfs'
but required in type 'Ns'.

在本节中,我们看到了子类型化的定义,并了解了可以在两种类型之间建立子类型化关系的两种方式:名义上(因为我们这么说)和结构上(因为类型具有相同的结构)。我们还看到,即使 TypeScript 使用结构子类型,我们也可以通过在结构子类型不合适的情况下使用唯一符号来模拟名义子类型。

In this section, we saw a definition of subtyping and learned about the two ways in which the subtyping relationship between two types can be established: nominally (because we say so) and structurally (because the types have the same structure). We also saw how, even though TypeScript uses structural subtyping, we can simulate nominal subtyping by using unique symbols for the situations in which structural subtyping is not appropriate.

7.1.3.练习

7.1.3. Exercises

1个

在 TypeScript 中,是定义为的类型 Painting的子类型Wine

类酒{
    名称:字符串;
    年份:数字;
}

类绘画{
    名称:字符串;
    年份:数字;
    画家:画家;
}

1

In TypeScript, is Painting a subtype of Wine for the types defined as

class Wine {
    name: string;
    year: number;
}

class Painting {
    name: string;
    year: number;
    painter: Painter;
}

2个

在 TypeScript 中,是定义为的类型 Car的子类型Wine

类酒{
    名称:字符串;
    年份:数字;
}

类车{
    制作:字符串;
    模型:字符串;
    年份:数字;
}

2

In TypeScript, is Car a subtype of Wine for the types defined as

class Wine {
    name: string;
    year: number;
}

class Car {
    make: string;
    model: string;
    year: number;
}

7.2. 分配给任何东西,分配给任何东西

7.2. Assigning anything to, assigning to anything

现在我们已经了解了子类型化,让我们看看两个极端:一个我们可以分配任何东西的类型和一个我们可以分配给任何东西的类型。第一个是我们可以用来存储任何东西的类型。第二种是我们可以使用的类型,如果我们没有其他类型的实例的话,我们可以使用它来代替任何其他类型。

Now that we’ve learned about subtyping, let’s look at a couple of extremes: a type to which we can assign anything and a type that we can assign to anything. The first one is a type we can use to store absolutely anything. The second is a type we can use instead of any other type if we don’t have an instance of that other type handy.

7.2.1.安全反序列化

7.2.1. Safe deserialization

我们在第 4 章unknown介绍了和类型。是一种可以存储任何其他类型的值的类型。我们提到其他面向对象的语言通常会提供一种以类似行为命名的类型。事实上,TypeScript也有类型;它提供了一些常用方法,例如. 但故事并没有就此结束,正如我们将在本节中看到的那样。 anyunknownObjectObjecttoString()

We covered the unknown and any types in chapter 4. unknown is a type that can store a value of any other type. We mentioned that other object-oriented languages usually provide a type named Object with similar behavior. In fact, TypeScript has an Object type too; it provides a few common methods such as toString(). But the story doesn’t end there, as we’ll see in this section.

any类型更危险。我们不仅可以给它赋值,还可以any给任何其他类型赋值,绕过类型检查。此类型用于与 JavaScript 代码的互操作性,但可能会产生意想不到的后果。假设我们有一个使用标准反序列化对象的函数JSON.parse(),如下一个清单所示。因为JSON.parse()它是一个 JavaScript 函数,它与 TypeScript 互操作,所以它不是强类型的;它的返回类型是any. 假设我们期望反序列化一个User具有name属性的实例。

The any type is more dangerous. We can not only assign any value to it, but also assign an any value to any other type, bypassing type checking. This type is used for interoperability with JavaScript code but may have unintended consequences. Suppose that we have a function that deserializes an object using the standard JSON.parse(), as shown in the next listing. Because JSON.parse() is a JavaScript function with which TypeScript interoperates, it is not strongly typed; its return type is any. Assume that we are expecting to deserialize a User instance that has a name property.

清单 7.5。反序列化any
类用户{
    名称:字符串;                              1个

    构造函数(名称:字符串){
        this.name = 名称;
    }
}

函数反序列化(输入:字符串):任何{
    返回 JSON.parse(输入);                  2个
}

功能问候(用户:用户):void {
    console.log(`嗨 ${user.name}!`);           3个
}

问候(反序列化('{“名称”:“爱丽丝”}'));     4
问候(反序列化('{}'));                      5个
class User {
    name: string;                              1

    constructor(name: string) {
        this.name = name;
    }
}

function deserialize(input: string): any {
    return JSON.parse(input);                  2
}

function greet(user: User): void {
    console.log(`Hi ${user.name}!`);           3
}

greet(deserialize('{ "name": "Alice" }'));     4
greet(deserialize('{}'));                      5

  • 1 用户类型有一个名称属性。
  • 1 The User type has a name property.
  • 2 deserialize() 简单地包装了 JSON.parse() 并返回一个 any 类型的值。
  • 2 deserialize() simply wraps JSON.parse() and returns a value of type any.
  • 3 greet() 使用给定用户对象的名称属性。
  • 3 greet() uses the name property of the given User object.
  • 4 我们反序列化一个有效的用户 JSON。
  • 4 We deserialize a valid User JSON.
  • 5 我们还可以反序列化不是 User 对象的对象。
  • 5 We can also deserialize an object that is not a User object.

最后一次调用greet()将记录日志"Hi undefined!",因为any绕过了类型检查,并且编译器允许我们将返回值视为 type 的值User,即使我们没有获得该类型的值。这个结果显然并不理想。我们需要在调用之前检查类型是否正确greet()

The last call to greet() will log "Hi undefined!" because any bypasses type checking, and the compiler allows us to treat the returned value as a value of type User, even when we didn’t get a value of that type. This result is clearly not ideal. We need to check that we have the right type before we call greet().

在这种情况下,我们要确保我们拥有的对象具有nametype 的属性string,在我们的情况下足以将其转换为User. 我们还应该检查我们的对象不是nullor undefined,它们是 TypeScript 中的特殊类型。这样做的一种方法是用这样的检查更新我们的代码,并在调用之前调用它greet()。请注意,此类型检查是在运行时完成的,因为它取决于输入值并且不是可以静态强制执行的内容。

In this case, we’d want to ensure that the object we have has a name property of type string, which in our case is enough to cast it into a User. We should also check that our object is not null or undefined, which are special types in TypeScript. One way of doing this is to update our code with such a check and call it before calling greet(). Note that this type check is done at run time, because it depends on the input value and is not something that can be enforced statically.

清单 7.6。运行时类型检查User
类用户{
    名称:字符串;

    构造函数(名称:字符串){
        this.name = 名称;
    }
}

函数反序列化(输入:字符串):任何{
    返回 JSON.parse(输入);
}

功能问候(用户:用户):void {
    console.log(`嗨 ${user.name}!`);
}

function isUser(user: any): user is User {            1
     if (user === null || user === undefined) 
        return false;

    return typeof user.name === 'string'; 
}

let user: any = deserialize('{ "name": "Alice" }');
如果(是用户(用户))                                    2
    问候(用户);

用户 = 未定义;
如果(isUser(用户))                                    2
    问候(用户);
class User {
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

function deserialize(input: string): any {
    return JSON.parse(input);
}

function greet(user: User): void {
    console.log(`Hi ${user.name}!`);
}

function isUser(user: any): user is User {           1
    if (user === null || user === undefined)
        return false;

    return typeof user.name === 'string';
}

let user: any = deserialize('{ "name": "Alice" }');
if (isUser(user))                                    2
    greet(user);

user = undefined;
if (isUser(user))                                    2
    greet(user);

  • 1 此函数检查给定参数是否为 User 类型。我们将具有字符串类型名称属性的变量视为用户类型。
  • 1 This function checks whether the given argument is of type User. We consider a variable with a name property of type string to be of User type.
  • 2 在每次使用前检查用户是否具有字符串类型的属性名称。
  • 2 Checks that user has a property name of type string before each use.

user is User返回类型isUser()是一些特定于 TypeScript 的语法,但我希望它不会太混乱。这种类型非常像boolean返回类型,但它对编译器具有额外的意义。如果函数返回true,则变量的user类型为User,编译器可以在调用方中使用该信息。实际上,在返回的每个if块中,具有类型而不是。 isUser()trueuserUserany

The user is User return type of isUser() is a bit of TypeScript-specific syntax, but I hope that it’s not too confusing. This type is very much like a boolean return type, but it carries extra meaning for the compiler. If the function returns true, the variable user has type User, and the compiler can use that information in the caller. Effectively, within each if block in which isUser() returned true, user has type User instead of any.

这种方法有效。当我们的用户名是 Alice 时,运行代码只会执行第一次调用。不会执行第二次调用,greet()因为在这种情况下,没有name属性user。但是,这种方法仍然存在一个问题:我们不是被迫执行此检查。因为没有强制执行,我们可能会犯错误而忘记调用它,这将允许任意结果从deserialize()进入greet(),并且没有什么可以阻止它这样做。

This approach works. Running the code executes only the first call when our username is Alice. The second call to greet() will not be executed because in this case, there is no name property on user. There’s still a problem with this approach, though: we are not forced to implement this check. Because no enforcement is going on, we could make a mistake and forget to call it, which would allow an arbitrary result from deserialize() to make its way to greet(), and there’s nothing to stop it from doing so.

如果我们有另一种说法,“这个对象绝对可以是任何类型”,但没有额外的“相信我,我知道我在做什么”暗示,那不是很好吗any?我们需要另一种类型——一种是系统中任何其他类型的超类型的类型,这意味着无论JSON.parse()返回什么,它都将是该类型的子类型。从那时起,类型系统将确保我们在将其转换为User.

Wouldn’t it be great if we had another way of saying, “This object can be of absolutely any type” but without the additional “Trust me, I know what I’m doing” that any implies? We need another type—a type that is a supertype of any other type in the system, which means that regardless of what JSON.parse()returns, it will be a subtype of this type. From there on, the type system will ensure that we add the proper type checking before we cast it to User.

顶级型

我们可以为其分配任何值的类型也称为顶级类型,因为任何其他类型都是该类型的子类型。换句话说,这种类型位于子类型层次结构的顶部(图 7.1)。

A type to which we can assign any value is also called a top type because any other type is a subtype of this type. In other words, this type sits at the top of the subtyping hierarchy (figure 7.1).

图 7.1。顶级类型是任何其他类型的超类型。我们可以定义任意数量的类型,但它们中的任何一个都将是顶级类型的子类型。我们可以在需要顶级类型的地方使用任何类型的值。

让我们更新我们的实现。我们可以从类型开始,它是类型系统中大多数Object类型的超类型,但有两个例外:和。TypeScript 类型系统具有一些强大的安全特性,其中之一就是能够将和值保留在其他类型的域之外。请记住第 3 章中的数十亿美元错误侧边栏——在大多数语言中,我们可以分配给任何类型。如果我们使用编译器标志(强烈推荐),这在 TypeScript 中是不允许的。TypeScript 认为是类型and 。所以我们的顶级类型,绝对是任何东西的超类型,是这三种类型的总和:nullundefinednullundefinednull--strictNullChecksnullnullundefined是类型undefinedObject | null | undefined. 这种类型实际上开箱即用地定义为unknown. 让我们重写代码以使用unknown,如下一个清单所示,然后我们可以讨论 usingany和之间的区别unknown

Let’s update our implementation. We can start with the Object type, which is the supertype of most types in the type systems, with two exceptions: null and undefined. The TypeScript type system has some great safety features, one of them being the ability to keep null and undefined values outside the domain of other types. Remember the billion-dollar-mistake sidebar in chapter 3—the fact that in most languages, we can assign null to any type. This is not allowed in TypeScript if we use the --strictNullChecks compiler flag (which is strongly recommended). TypeScript considers null to be of type null and undefined to be of type undefined. So our top type, the supertype of absolutely anything, is the sum of these three types: Object | null | undefined. This type is actually defined out of the box as unknown. Let’s rewrite our code to use unknown, as shown in the next listing, and then we can discuss the differences between using any and unknown.

清单 7.7。使用更强的类型unknown
类用户{
    名称:字符串;

    构造函数(名称:字符串){
        this.name = 名称;
    }
}

函数反序列化(输入:字符串):未知{             1
    返回 JSON.parse(输入);
}

功能问候(用户:用户):void {
    console.log(`嗨 ${user.name}!`);
}

function isUser(user: any): 用户是用户 {                 2
    如果(用户 === 空 || 用户 === 未定义)
        返回假;

    return typeof user.name === 'string';
}

让用户:未知=反序列化('{“名称”:“爱丽丝”}');   3个
如果(是用户(用户))
    问候(用户);

用户=反序列化(“空”);
如果(是用户(用户))
    问候(用户);
class User {
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

function deserialize(input: string): unknown {            1
    return JSON.parse(input);
}

function greet(user: User): void {
    console.log(`Hi ${user.name}!`);
}

function isUser(user: any): user is User {                2
    if (user === null || user === undefined)
        return false;

    return typeof user.name === 'string';
}

let user: unknown = deserialize('{ "name": "Alice" }');   3
if (isUser(user))
    greet(user);

user = deserialize("null");
if (isUser(user))
    greet(user);

  • 1 我们使 deserialize() 返回未知。
  • 1 We make deserialize() return unknown.
  • 2 我们将 isUser() 参数保持不变。
  • 2 We keep the isUser() argument as any.
  • 3 我们将变量声明为类型未知。
  • 3 We declare our variable as having type unknown.

这种变化是微妙但强大的:一旦我们从 获得一个值JSON.parse(),我们就将它从 转换anyunknown。这个过程是安全的,因为任何东西都可以转换为unknown. isUser()我们保留as的参数any,因为它使我们的实现更容易。(我们不允许typeof user.name在没有额外转换的情况下执行诸如 an之类的检查unknown。)

The change is subtle but powerful: as soon as we get a value from JSON.parse(), we convert it from any to unknown. This process is safe, because anything can be converted to unknown. We keep the argument of isUser() as any, because it makes our implementation easier. (We wouldn’t be allowed to perform a check such as typeof user.name on an unknown without extra casting.)

代码像以前一样工作,区别在于如果我们删除任何调用isUser(),代码将不再编译。编译器发出以下错误:

The code works as before, the distinction being that if we remove any of the isUser() calls, the code no longer compiles. The compiler issues the following error:

“未知”类型的参数不可分配给参数
类型为“用户”。
Argument of type 'unknown' is not assignable to parameter
of type 'User'.

我们不能简单地将类型变量传递给unknowngreet()它需要一个User. 该函数isUser()很有帮助,因为每当它返回时true,编译器都会自动将变量视为类型User

We can’t simply pass a variable of type unknown to greet(), which expects a User. The function isUser() helps, as whenever it returns true, the compiler automatically considers the variable to have type User.

有了这个实现,我们就不能忘记检查;编译器不允许我们。它允许我们仅User在确认user is User.

With this implementation, we simply cannot forget to check; the compiler will not allow us. It allows us to use our object as a User only after we confirm that user is User.

未知和任何之间的区别

unknown尽管我们可以为和分配任何内容any,但是我们使用其中一种类型的变量的方式有所不同。在这种情况下,只有在我们确认该值确实具有该类型(就像我们对将用户返回为 的函数所做的那样)之后unknown,我们才能将该值用作某种类型(例如 )。在这种情况下,我们可以立即将该值用作任何其他类型的值。绕过类型检查。 UserUseranyany

Although we can assign anything to both unknown and any, there is a difference in how we use a variable of one of these types. In the unknown case, we can use the value as some type (such as User) only after we confirm that the value actually has that type (as we did with the function that returns the user as User). In the any case, we can use the value as a value of any other type right away. any bypasses type checking.

其他语言提供不同的机制来确定值是否属于给定类型。is例如,C# 有关键字,而 Java 有instanceof. 通常,当我们处理可以是任何值的值时,我们首先将其视为顶级类型。然后我们使用适当的检查来确保它是我们需要的类型,然后再将其向下转换为所需的类型。

Other languages provide different mechanisms to determine whether a value is of a given type. C# has the is keyword, for example, and Java has instanceof. In general, when we deal with a value that could be anything, we start by considering it to be a top type. Then we use the appropriate checks to ensure that it is of the type we need before we downcast it to the required type.

7.2.2.错误案例的值

7.2.2. Values for error cases

现在让我们看一个相反的问题:一个可以代替任何其他类型使用的类型。让我们以清单 7.8中的一个简单示例为例。在我们的游戏中,我们可以转动我们的宇宙飞船LeftRight。我们会将这些可能的方向表示为枚举。我们想要实现一个函数,它接受一个方向并将其转换为我们旋转宇宙飞船的角度。因为我们要确保涵盖所有情况,所以如果枚举的值与两个预期值LeftRight值不同,我们将抛出错误。

Now let’s look at an opposite problem: a type that can be used instead of any other type. Let’s take a simple example in listing 7.8. In our game, we can turn our spaceship Left or Right. We’ll represent these possible directions as an enumeration. We want to implement a function that takes a direction and converts it to an angle by which we rotate our spaceship. Because we want to make sure that we cover all cases, we’ll throw an error if the enumeration has a value different from the two expected Left and Right values.

清单 7.8。TurnDirection到角度转换
枚举转弯方向 {
    左边,
    正确的
}

函数 turnAngle(turn: TurnDirection): number {
    开关(转){
        case TurnDirection.Left: 返回 -90;                  1
        例 TurnDirection.Right:返回 90;                  1
        默认值:throw new Error("Unknown TurnDirection");    2个
    }
}
enum TurnDirection {
    Left,
    Right
}

function turnAngle(turn: TurnDirection): number {
    switch (turn) {
        case TurnDirection.Left: return -90;                  1
        case TurnDirection.Right: return 90;                  1
        default: throw new Error("Unknown TurnDirection");    2
    }
}

  • 1 左转变为 –90 度;右转变为 90 度。
  • 1 A Left turn becomes –90 degrees; a Right turn becomes 90 degrees.
  • 2 如果遇到意外值,我们会抛出错误。
  • 2 We throw an error in case we encounter an unexpected value.

到目前为止,一切都很好。但是如果我们有一个处理错误场景的函数呢?假设我们想在抛出错误之前记录错误。这个函数总是会抛出异常,所以我们将它声明为返回类型never,正如我们在第 2 章中看到的那样。提醒一下,never是不能赋值的空类型。我们用它来明确地表明一个函数永远不会返回,要么是因为它永远循环,要么是因为它抛出异常,如下一个清单所示。

So far, so good. But what if we have a function that handles error scenarios? Suppose that we want to log the error before throwing it. This function would always throw, so we’ll declare it as returning the type never, as we saw in chapter 2. As a reminder, never is the empty type that cannot be assigned any value. We use it to explicitly show that a function never returns, either because it loops forever or because it throws, as shown in the next listing.

清单 7.9。报错
function fail(message: string): never {       1 
    console.error(message);                  2
    抛出新的错误(消息);                2 
}
function fail(message: string): never {      1
    console.error(message);                  2
    throw new Error(message);                2
}

  • 1 fail() 从不返回(总是抛出),所以我们声明它从不返回。
  • 1 fail() never returns (always throws), so we declare it as returning never.
  • 2 将错误打印到控制台,然后抛出。
  • 2 Print error to console and then throw.

如果我们想用 替换语句throw,我们最终会得到类似下面的内容。 turnAngle()fail()

If we want to replace the throw statement in turnAngle() with fail(), we end up with something like the following.

清单 7.10。turnAngle()使用fail()
函数 turnAngle(turn: TurnDirection): number {
    开关(转){
        case TurnDirection.Left: 返回 -90;
        case TurnDirection.Right: 返回 90;
        默认值:失败(“未知转向”);       1个
    }
}
function turnAngle(turn: TurnDirection): number {
    switch (turn) {
        case TurnDirection.Left: return -90;
        case TurnDirection.Right: return 90;
        default: fail("Unknown TurnDirection");       1
    }
}

  • 1 我们用对 fail() 的调用替换 throw。
  • 1 We replace throw with a call to fail().

这段代码几乎可以工作,但不完全是。在严格模式下(带--strict标志)编译失败,出现以下错误:

This code almost works, but not quite. Compilation fails in strict mode (with --strict flag) with the following error:

函数缺少结束返回语句和返回类型
不包括“未定义”。
Function lacks ending return statement and return type
does not include "undefined".

编译器看不到分支return上的语句default并将其标记为错误。一个修复方法是返回一个虚拟值,如下一个清单所示,知道我们无论如何都会在到达它之前抛出。

The compiler doesn’t see a return statement on the default branch and flags that as an error. One fix would be to return a dummy value as shown in the next listing, knowing that we throw before reaching it anyway.

清单 7.11。turnAgain()使用fail()并返回一个虚拟值
枚举转弯方向 {
    左边,
    正确的
}

函数 turnAngle(turn: TurnDirection): number {
    开关(转){
        case TurnDirection.Left: 返回 -90;
        case TurnDirection.Right: 返回 90;
        默认: {
            失败(“未知转向”);
            返回-1;                       1个
        }
    }
}
enum TurnDirection {
    Left,
    Right
}

function turnAngle(turn: TurnDirection): number {
    switch (turn) {
        case TurnDirection.Left: return -90;
        case TurnDirection.Right: return 90;
        default: {
            fail("Unknown TurnDirection");
            return -1;                       1
        }
    }
}

  • 1 永远不会实际返回的虚拟值,因为 fail() 抛出
  • 1 Dummy value that will never actually be returned because fail() throws

但是,如果在未来的某个时候,我们fail()以一种它并不总是抛出的方式更新呢?那么我们的代码最终会返回一个虚拟值,即使它永远不会这样做。有一个更好的解决方案:返回 的结果fail(),如以下清单所示。

But what if, at some point in the future, we update fail()in such a way that it doesn’t always throw? Then our code would end up returning a dummy value, even though it should never do so. There’s a better solution: return the result of fail(), as the following listing shows.

清单 7.12。turnAngle()使用fail()并返回其结果
函数 turnAngle(turn: TurnDirection): number {
    开关(转){
        case TurnDirection.Left: 返回 -90;
        case TurnDirection.Right: 返回 90;
        默认值:return fail("Unknown TurnDirection");      1个
    }
}
function turnAngle(turn: TurnDirection): number {
    switch (turn) {
        case TurnDirection.Left: return -90;
        case TurnDirection.Right: return 90;
        default: return fail("Unknown TurnDirection");      1
    }
}

  • 1 只返回 fail() 返回的任何内容。
  • 1 Just return whatever fail() returns.

这段代码之所以有效,是因为除了是没有值的类型之外,never该类型还是系统中所有其他类型的子类型。

The reason why this code works is that besides being the type without values, never is the type that is the subtype of all other types in the system.

底部类型

作为任何其他类型的子类型的类型称为底部类型,因为它位于子类型层次结构的底部。要成为任何其他可能类型的子类型,它必须具有任何其他可能类型的成员。因为我们可以有无限数量的类型和成员,所以底层类型也必须有无限数量的成员。因为那是不可能的,底层类型总是一个空类型:我们不能为其创建实际值的类型(图 7.2)。

A type that is the subtype of any other type is called a bottom type because it sits at the bottom of the subtyping hierarchy. To be a subtype of any other possible type, it must have the members of any other possible type. Because we can have an infinite number of types and members, the bottom type would also have to have an infinite number of members. Because that is impossible, the bottom type is always an empty type: a type for which we can’t create an actual value (figure 7.2).

图 7.2。底层类型是任何其他类型的子类型。我们可以定义任意数量的类型,但其中任何一个都将是底层类型的超类型。我们可以在任何需要任何类型的值的地方传递一个底层类型的值(尽管我们永远不能产生这样的值)。

因为我们可以分配never给任何其他类型,因为它是底层类型,所以我们可以从函数中返回它。编译器不会抱怨,因为这是一个向上转换(将值从子类型转换为超类型),它可以隐式完成。我们说,“把这个不可能创建的值变成一个字符串,”这很好。因为该fail()函数永远不会返回,所以我们永远不会遇到实际需要将某些内容转换为字符串的情况。

Because we can assign never to any other type, due to it being a bottom type, we can return it from the function. The compiler will not complain, as this is an upcast (converting a value from a subtype to a supertype), which can be done implicitly. We’re saying, “Take this value that is impossible to create and turn it into a string,” which is fine. Because the fail() function never returns, we never end up in a situation in which we actually have something to turn into a string.

这种方法比前一种方法更好,因为如果我们更新fail()以致在某些情况下它不更新throw,编译器将强制我们修复所有代码。第一的,fail()它会迫使我们将from的返回类型更改never为其他类型,例如void. 然后它会看到我们正在尝试将其作为 a 传递string,它不进行类型检查。我们将不得不更新我们的实现turnAngle(),也许是通过恢复显式throw.

This approach is better than the preceding one because, if we update fail() so that it doesn’t throw in some cases, the compiler will force us to fix all our code. First, it will force us to change the return type of fail() from never to something else, such as void. Then it will see that we are trying to pass that as a string, which does not type-check. We will have to update our implementation of turnAngle(), perhaps by bringing back the explicit throw.

底部类型允许我们假装我们有任何类型的值,即使我们不能想出一个。

A bottom type allows us to pretend that we have a value of any type even if we can’t come up with one.

7.2.3.顶部和底部类型回顾

7.2.3. Top and bottom types recap

让我们快速回顾一下我们在本节中介绍的内容。两种类型可以处于子类型化关系中,其中一个是超类型,另一个是子类型。在极端情况下,我们有一个类型是任何其他类型的超类型和一个类型是任何其他类型的子类型。

Let’s quickly recap what we covered in this section. Two types can be in a subtyping relationship, in which one of them is the supertype and the other is the subtype. At the extreme, we have a type that is the supertype of any other type and a type that is the subtype of any other type.

任何其他类型的超类型,称为顶级类型,可用于保存任何其他类型的值。该类型unknown在 TypeScript 中。这会派上用场的一种情况是,当我们处理可以是任何数据的数据时,例如从 NoSQL 数据库读取的 JSON 文档。我们最初将此类数据键入顶级类型,然后执行所需的检查以将其转换为我们可以使用的类型。

The supertype of any other type, called the top type, can be used to hold a value of any other type. That type is unknown in TypeScript. One situation in which this comes in handy is when we are dealing with data that can be anything, such as as a JSON document read from a NoSQL database. We initially type such data as the top type and then perform the required checks to cast it down to a type we can work with.

任何其他类型的子类型,称为底层类型,可用于产生任何其他类型的值。这种类型never在 TypeScript 中。一个示例应用程序在无法通过始终抛出的函数生成返回值时生成返回值。

The subtype of any other type, called the bottom type, can be used to produce a value of any other type. This type is never in TypeScript. One example application is producing a return value when none can be produced via a function that always throws.

请注意,尽管大多数主流语言都提供顶层类型,但很少有主流语言提供底层类型。我们在第 2 章中看到的 DIY 实现使类型为空但不为底部。除非进入编译器,否则无法定义我们的自定义底部类型。

Note that although most mainstream languages provide a top type, few of them provide a bottom type. The DIY implementation we saw in chapter 2 makes a type empty but not bottom. Unless worked into the compiler, there is no way to define our custom bottom type.

接下来,让我们看看更复杂类型的子类型化,看看它是如何工作的。

Next, let’s look at subtyping for more complex types and see how that works.

7.2.4.练习

7.2.4. Exercises

1个

如果我们有一个makeNothing()返回 的函数never,我们可以用它的结果初始化一个x类型的变量吗number(不强制转换)?

声明函数 makeNothing(): 从不;

让 x: number = makeNothing();

1

If we have a function makeNothing() that returns never, can we initialize a variable x of type number with its result (without casting)?

declare function makeNothing(): never;

let x: number = makeNothing();

2个

如果我们有一个makeSomething()返回 的函数unknown,我们可以用它的结果初始化一个x类型的变量吗number(不强制转换)?

声明函数 makeSomething(): 未知;

让 x: number = makeSomething();

2

If we have a function makeSomething() that returns unknown, can we initialize a variable x of type number with its result (without casting)?

declare function makeSomething(): unknown;

let x: number = makeSomething();

7.3. 允许替换

7.3. Allowed substitutions

到目前为止,我们已经看过几个简单的子类型示例。例如,我们观察到如果Triangle extends Shape,Triangle是 的子类型Shape。现在让我们尝试回答一些更棘手的问题:

So far, we’ve looked at a few simple examples of subtyping. We observed, for example, that if Triangle extends Shape, Triangle is a subtype of Shape. Now let’s try to answer a few trickier questions:

  • Triangle | Square求和类型和之间的子类型关系是什么Triangle | Square | Circle
  • What is the subtyping relationship between the sum types Triangle | Square and Triangle | Square | Circle?
  • 三角形数组 ( Triangle[]) 和形状数组 ( Shape[]) 之间的子类型关系是什么?
  • What is the subtyping relationship between an array of triangles (Triangle[]) and an array of shapes (Shape[])?
  • 通用数据结构(例如List<T>、forList<Triangle>和 )之间的子类型关系是什么List<Shape>
  • What is the subtyping relationship between a generic data structure such as List<T>, for List<Triangle> and List<Shape>?
  • 函数类型() => Shape() => Triangle
  • What about the function types () => Shape and () => Triangle?
  • (argument: Shape) => void反之,函数类型和函数类型呢(argument: Triangle) => void
  • Conversely, what about the function type (argument: Shape) => void and the function type (argument: Triangle) => void?

这些问题很重要,因为它们告诉我们这些类型中的哪些可以替代它们的子类型。每当我们看到一个需要这些类型之一的参数的函数时,我们应该了解我们是否可以提供一个子类型。

These questions are important, as they tell us which of these types can be substituted for their subtypes. Whenever we see a function that expects an argument of one of these types, we should understand whether we can provide a subtype instead.

前面示例中的挑战在于事情并不像Triangle extends Shape. 我们正在研究基于Triangle和定义的类型ShapeTriangle并且Shape是总和类型、集合元素类型或函数参数类型或返回类型的一部分。

The challenge in the preceding examples is that things aren’t as straightforward as Triangle extends Shape. We are looking at types that are defined based on Triangle and Shape. Triangle and Shape are part of the sum types, the types of elements of a collection, or a function’s argument types or return types.

7.3.1.子类型和求和类型

7.3.1. Subtyping and sum types

我们先举一个最简单的例子:sum 类型。假设我们有一个draw()可以绘制 a Triangle、 aSquare或 a 的函数Circle。我们可以传递 a TriangleorSquare给它吗?正如您可能已经猜到的那样,答案是肯定的。我们可以在下面的清单中检查这样的代码是否编译。

Let’s take the simplest example first: the sum type. Suppose that we have a draw() function that can draw a Triangle, a Square, or a Circle. Can we pass a Triangle or Square to it? As you might have guessed, the answer is yes. We can check that such code compiles in the following listing.

清单 7.13。Triangle | Square作为Triangle | Square | Circle
声明 const TriangleType:唯一符号;
类三角形{
    [三角形类型]: void;
    /* 三角成员 */
}

声明 const SquareType:唯一符号;
类广场{
    [方形]: void;
    /* 方形成员 */
}

声明 const CircleType:唯一符号;
类圈子{
    [圆类型]: void;
    /* 圈子成员 */
}

声明函数 makeShape(): 三角形 | 正方形;                  1
声明函数 draw(shape: Triangle | Square | Circle): void;   2个

绘制(制作形状());
declare const TriangleType: unique symbol;
class Triangle {
    [TriangleType]: void;
    /* Triangle members */
}

declare const SquareType: unique symbol;
class Square {
    [SquareType]: void;
    /* Square members */
}

declare const CircleType: unique symbol;
class Circle {
    [CircleType]: void;
    /* Circle members */
}

declare function makeShape(): Triangle | Square;                  1
declare function draw(shape: Triangle | Square | Circle): void;   2

draw(makeShape());

  • 1 makeShape() 返回三角形或正方形(省略实现)。
  • 1 makeShape() returns a Triangle or a Square (implementation omitted).
  • 2 draw() 接受三角形、正方形或圆形(省略实现)。
  • 2 draw() accepts a Triangle, a Square, or a Circle (implementation omitted).

我们在这些示例中强制执行标称子类型化,因为我们没有为这些类型提供完整的实现。在实践中,它们会有各种不同的属性和方法来区分它们。我们在示例中使用独特的符号来模拟这些不同的属性,因为由于 TypeScript 的结构子类型化,将类留空会使它们全部等效。

We enforce nominal subtyping throughout these examples because we’re not providing full implementations for these types. In practice, they would have various different properties and methods to distinguish them. We simulate these different properties with unique symbols for our examples, as leaving the classes empty would make all of them equivalent due to TypeScript’s structural subtyping.

正如预期的那样,此代码可以编译。反之则不然:如果我们可以绘制 aTriangle或 aSquare并尝试绘制 a Triangle, Square, or Circle,编译器会报错,因为我们可能最终将 a 传递Circledraw()函数,而函数不知道如何处理它。我们可以确认以下代码无法编译。

As expected, this code compiles. The opposite doesn’t: if we can draw a Triangle or a Square and attempt to draw a Triangle, Square, or Circle, the compiler will complain, because we might end up passing a Circle to the draw() function, which wouldn’t know what to do with it. We can confirm that the following code doesn’t compile.

清单 7.14。Triangle | Square | Circle作为Triangle | Square
声明函数 makeShape():三角形 | 方形 | 圆圈1
声明函数 draw(shape: Triangle | Square ): void;       1个

绘制(制作形状());                                           2个
declare function makeShape(): Triangle | Square | Circle;    1
declare function draw(shape: Triangle | Square): void;       1

draw(makeShape());                                           2

  • 1 我们翻转了类型,使 makeShape() 也可以返回 Circle,而 draw() 不再接受 Circle。
  • 1 We flipped the types so that makeShape() could also return a Circle, whereas draw() no longer accepts a Circle.
  • 2 这不再编译。
  • 2 This no longer compiles.

Triangle | Square是 的子类型Triangle | Square | Circle:我们总是可以用 aTriangle或代替Squarea Triangle, Square, orCircle但不能反过来。

Triangle | Square is a subtype of Triangle | Square | Circle: we can always substitute a Triangle or Square for a Triangle, Square, or Circle but not the other way around.

这种情况似乎有悖常理,因为Triangle | Square“小于” Triangle | Square | Circle。每当我们使用继承时,我们最终都会得到一个比其超类型具有更多属性的子类型。对于 sum 类型,它以相反的方式工作:超类型比子类型具有更多类型(图 7.3)。

This situation may seem to be counterintuitive, because Triangle | Square is “less” than Triangle | Square | Circle. Whenever we use inheritance, we end up with a subtype that has more properties than its supertype. For sum types, it works the opposite way: the supertype has more types than the subtype (figure 7.3).

图 7.3。Triangle | Square是的子类型Triangle | Square | Circle,因为只要需要a TriangleSquare或,我们就可以使用 a或 a 。 CircleTriangleSquare

假设我们有一个EquilateralTriangle继承自 的Triangle,如下一个清单所示。

Say we have an EquilateralTriangle which inherits from Triangle, as shown in the next listing.

清单 7.15。EquilateralTriangle宣言
声明 const EquilateralTriangleType:唯一符号;
类 EquilateralTriangle 扩展三角形 {
    [等边三角形类型]: void;
    /* 等边三角形成员 */
}
declare const EquilateralTriangleType: unique symbol;
class EquilateralTriangle extends Triangle {
    [EquilateralTriangleType]: void;
    /* EquilateralTriangle members */
}

作为练习,检查当我们将求和类型与继承混合时会发生什么。makeShape()退货EquilateralTriangle | Squaredraw()接受Triangle | Square | Circle工作吗?makeShape()退货Triangle | Squaredraw()接受怎么样EquilateralTriangle | Square | Circle

As an exercise, check what happens when we mix sum types with inheritance. Does makeShape() returning EquilateralTriangle | Square and draw() accepting Triangle | Square | Circle work? What about makeShape() returning Triangle | Square and draw() accepting EquilateralTriangle | Square | Circle?

这种形式的子类型是编译器必须支持的。使用我们在第 3 章Variant中看到的DIY 求和类型,我们不会得到相同的子类型行为。请记住,可以包装多种类型之一的值,但它本身不是这些类型中的任何一种。 Variant

This form of subtyping is something that has to be supported by the compiler. With a DIY sum type like the Variant we looked at in chapter 3, we would not get the same subtyping behavior. Remember the Variant can wrap a value of one of several types, but it is not itself any of those types.

7.3.2.子类型和集合

7.3.2. Subtyping and collections

现在让我们看看包含一些其他类型的一组值的类型。让我们从下一个清单中的数组开始。如果是 的子类型,我们可以将Triangle对象数组传递给接受对象draw()数组的函数吗? ShapeTriangleShape

Now let’s look at types that contain a set of values of some other type. Let’s start with arrays in the next listing. Can we pass an array of Triangle objects to a draw() function that accepts an array of Shape objects if Triangle is a subtype of Shape?

清单 7.16。Triangle[]作为Shape[]
类形状{
    /* 形状成员 */
}

声明 const TriangleType:唯一符号;
三角形类扩展形状 {                    1
    [三角形类型]: void;
    /* 三角成员 */
}

声明函数 makeTriangles(): Triangle[];    2
声明函数 draw(shapes: Shape[]): void;    3个

绘制(制作三角形());                           4个
class Shape {
    /* Shape members */
}

declare const TriangleType: unique symbol;
class Triangle extends Shape {                   1
    [TriangleType]: void;
    /* Triangle members */
}

declare function makeTriangles(): Triangle[];    2
declare function draw(shapes: Shape[]): void;    3

draw(makeTriangles());                           4

  • 1 三角形是形状的子类型。
  • 1 Triangle is a subtype of Shape.
  • 2 makeTriangles() 返回一个三角形对象数组。
  • 2 makeTriangles() returns an array of Triangle objects.
  • 3 draw() 接受一组 Shape 对象。
  • 3 draw() accepts an array of Shape objects.
  • 4 我们可以将 Triangle 对象数组用作 Shape 对象数组。
  • 4 We can use an array of Triangle objects as an array of Shape objects.

这种观察可能并不令人惊讶,但它很重要:数组保留了它们所存储的基础类型的子类型关系。正如预期的那样,相反的情况不起作用:如果我们在需要对象Shape数组时尝试传递对象数组Triangle,代码将无法编译(图 7.4)。

This observation may not be surprising, but it is important: arrays preserve the subtyping relationship of the underlying types that they are storing. As expected, the opposite doesn’t work: if we try to pass an array of Shape objects when an array of Triangle objects is expected, the code won’t compile (figure 7.4).

图 7.4。如果Triangle是 的子类型Shape,则三角形数组是形状数组的子类型。如果我们可以将 a 用作Trianglea Shape,我们就可以将Triangle对象数组用作对象数组Shape

正如我们在第 2 章中看到的,数组是许多编程语言中现成的基本类型。如果我们定义一个自定义集合,比如一个LinkedList<T>

As we saw in chapter 2, arrays are basic types that come out of the box in many programming languages. What if we define a custom collection, such as a LinkedList<T>?

清单 7.17。LinkedList<Triangle>作为LinkedList<Shape>
类 LinkedList<T> {                                       1
    值:T;
    下一个:链表<T> | 未定义=未定义;

    构造函数(值:T){
        this.value = 值;
    }

    追加(值:T):LinkedList<T> {
        this.next = new LinkedList(value);
        返回这个。下一个;
    }
}

声明函数 makeTriangles(): LinkedList<Triangle>;    2
声明函数 draw(shapes: LinkedList<Shape>): void;    3个

绘制(制作三角形());                                     4个
class LinkedList<T> {                                      1
    value: T;
    next: LinkedList<T> | undefined = undefined;

    constructor(value: T) {
        this.value = value;
    }

    append(value: T): LinkedList<T> {
        this.next = new LinkedList(value);
        return this.next;
    }
}

declare function makeTriangles(): LinkedList<Triangle>;    2
declare function draw(shapes: LinkedList<Shape>): void;    3

draw(makeTriangles());                                     4

  • 1 通用链表集合
  • 1 A generic linked list collection
  • 2 makeTriangle() 现在返回三角形的链表。
  • 2 makeTriangle() now returns a linked list of triangles.
  • 3 draw() 接受形状的链表。
  • 3 draw() accepts a linked list of shapes.
  • 4 代码编译。
  • 4 The code compiles.

即使没有基本类型,TypeScript 也会正确地确定它LinkedList-<Triangle>LinkedList<Shape>. 和以前一样,相反的不会编译;我们不能将 aLinkedList<Shape>作为 a传递LinkedList<Triangle>

Even without a primitive type, TypeScript correctly establishes that LinkedList-<Triangle> is a subtype of LinkedList<Shape>. As before, the opposite doesn’t compile; we can’t pass a LinkedList<Shape> as a LinkedList<Triangle>.

协方差

保留其基础类型的子类型化关系的类型称为协变。数组是协变的,因为它保留了子类型关系:Triangle是 的子类型Shape,也是Triangle[]的子类型Shape[]

A type that preserves the subtyping relationship of its underlying type is called covariant. An array is covariant because it preserves the subtyping relationship: Triangle is a subtype of Shape, so Triangle[] is a subtype of Shape[].

在处理数组和集合(例如LinkedList<T>. 例如,在 C# 中,我们必须显式声明类型的协变性,例如LinkedList<T>通过声明接口和使用out关键字 ( ILinkedList<out T>)。否则,编译器将不会推导出子类型关系。

Various languages behave differently when dealing with arrays and collections such as LinkedList<T>. In C#, for example, we would have to explicitly state covariance for a type such as LinkedList<T> by declaring an interface and using the out keyword (ILinkedList<out T>). Otherwise, the compiler will not deduce the subtyping relationship.

协变的替代方法是简单地忽略两个给定类型之间的子类型关系,并考虑 aLinkedList<Shape>LinkedList<Triangle>是它们之间没有子类型关系的类型。(两者都不是另一个的子类型。)在 TypeScript 中不是这种情况,但在 C# 中是这样,其中 aList<Shape>和 aList<Triangle>没有子类型关系。

An alternative to covariance is to simply ignore the subtyping relationship between two given types and consider a LinkedList<Shape> and LinkedList<Triangle> to be types with no subtyping relationship between them. (Neither is a subtype of the other.) This is not the case in TypeScript, but it is in C#, in which a List<Shape> and a List<Triangle> have no subtyping relationship.

不变性

忽略其基础类型的子类型化关系的类型称为不变的。AC#List<T>是不变的,因为它忽略了子类型关系"Triangle is a subtype of Shape",因此List<Shape>List<Triangle>没有子类型-超类型关系。

A type that ignores the subtyping relationship of its underlying type is called invariant. A C# List<T> is invariant because it ignores the subtyping relationship "Triangle is a subtype of Shape", so List<Shape> and List<Triangle> have no subtype–supertype relationship.

现在我们已经了解了集合如何在子类型方面相互关联,并且已经看到了两种常见的差异类型,让我们看看函数类型是如何相关的。

Now that we’ve looked at how collections relate to one another in terms of subtyping and have seen two common types of variance, let’s see how function types are related.

7.3.3.子类型和函数返回类型

7.3.3. Subtyping and function return types

Triangle我们首先从更简单的情况开始:让我们看看我们可以在返回 a 的函数和返回 a 的函数之间进行哪些替换Shape,如清单 7.18所示。我们将声明两个工厂函数:makeShape()返回 a 的 aShapemakeTriangle()返回 a 的 a Triangle

We’ll start with the simpler case first: let’s see what substitutions we can make between a function that returns a Triangle and a function that returns a Shape, as shown in listing 7.18. We’ll declare two factory functions: a makeShape() that returns a Shape and a makeTriangle() that returns a Triangle.

然后我们将实现一个useFactory()函数,该函数将 type 的函数() => Shape作为参数并返回一个Shape. 我们将尝试传递makeTriangle()给它。

Then we’ll implement a useFactory() function that takes a function of type () => Shape as argument and returns a Shape. We’ll try passing makeTriangle() to it.

清单 7.18。() => Triangle作为() => Shape
声明函数 makeTriangle(): 三角形;
声明函数 makeShape(): 形状;

function useFactory(factory: () => Shape): Shape {      1 
    return factory();                                  1个
}

让 shape1: Shape = useFactory(makeShape);             2
让 shape2: Shape = useFactory(makeTriangle);          2个
declare function makeTriangle(): Triangle;
declare function makeShape(): Shape;

function useFactory(factory: () => Shape): Shape {     1
    return factory();                                  1
}

let shape1: Shape = useFactory(makeShape);             2
let shape2: Shape = useFactory(makeTriangle);          2

  • 1 useFactory() 接受一个不带参数的函数,该函数返回一个 Shape 并调用它。
  • 1 useFactory() takes a function with no arguments that returns a Shape and calls it.
  • 2 makeTriangle() 和 makeShape() 都可以用作 useFactory() 的参数。
  • 2 Both makeTriangle() and makeShape() can be used as arguments to useFactory().

这里没有什么不寻常的:我们可以将返回 a 的函数Triangle作为返回 a 的函数传递,Shape因为返回值 (a Triangle) 是 的子类型Shape,因此我们可以将其分配给 a Shape图 7.5)。

Nothing is out of the ordinary here: we can pass a function that returns a Triangle as a function that returns a Shape because the return value (a Triangle) is a subtype of Shape, so we can assign it to a Shape (figure 7.5).

图 7.5。如果Triangle是 的子类型Shape,我们可以使用返回 a 的函数Triangle而不是返回 a 的函数Shape,因为我们总是可以将 a 分配Triangle给期望 a 的调用者Shape

相反的是行不通的:如果我们改变我们useFactory()期待一个() => Triangle参数并尝试传递它makeShape(),下面的代码将无法编译。

The opposite doesn’t work: if we change our useFactory() to expect a () => Triangle argument and try to pass it makeShape(), the following code won’t compile.

清单 7.19。() => Shape作为() => Triangle
声明函数 makeTriangle(): 三角形;
声明函数 makeShape(): 形状;

function useFactory(factory: () => Triangle ):三角形{     1
    返回工厂();
}

让 shape1: Shape = useFactory(makeShape);                  2
让 shape2: Shape = useFactory(makeTriangle);
declare function makeTriangle(): Triangle;
declare function makeShape(): Shape;

function useFactory(factory: () => Triangle): Triangle {    1
    return factory();
}

let shape1: Shape = useFactory(makeShape);                  2
let shape2: Shape = useFactory(makeTriangle);

  • 1 我们在这里将 Shape 替换为 Triangle。
  • 1 We replace Shape with Triangle here.
  • 2 代码编译失败;我们不能将 makeShape() 用作 () => 三角形。
  • 2 Code fails to compile; we can’t use makeShape() as a () => Triangle.

同样,这段代码非常简单:我们不能使用makeShape()as 类型的函数() => Triangle,因为makeShape()它返回一个Shape对象。该对象可能是 a Triangle,但也可能是 a SquareuseFactory()承诺返回 a Triangle,因此它不能返回 的超类型Triangle。当然,它可以返回一个子类型,例如EquilateralTriangle给定一个makeEquilateralTriangle().

Again, this code is pretty straightforward: we can’t use makeShape() as a function of type () => Triangle because makeShape() returns a Shape object. That object could be a Triangle, but it also might be a Square. useFactory() promises to return a Triangle, so it can’t return a supertype of Triangle. It could, of course, return a subtype such as EquilateralTriangle, given a makeEquilateralTriangle().

函数的返回类型是协变的。换句话说,如果Triangle是 的子类型Shape,则函数类型如() => Triangle是 a 的子类型function () => Shape。请注意,函数类型不必描述不带任何参数的函数。如果 和makeTriangle()makeShape()接受了几个number参数,它们仍然是协变的,正如我们刚刚看到的那样。

Functions are covariant in their return types. In other words, if Triangle is a subtype of Shape, a function type such as () => Triangle is a subtype of a function () => Shape. Note that the function types don’t have to describe functions that don’t take any arguments. If both makeTriangle() and makeShape() took a couple of number arguments, they would still be covariant, as we just saw.

大多数主流编程语言都遵循这种行为。重写继承类型中的方法,更改它们的返回类型,遵循相同的规则。如果我们实现一个ShapeMaker提供make()返回 a 的方法的类Shape,我们可以在派生类中覆盖它MakeTriangle以返回Triangle,如以下清单所示。编译器允许这样做,因为调用任何一个make()方法都会给我们一个Shape对象。

This behavior is followed by most mainstream programming languages. The same rules are followed for overriding methods in inherited types, changing their return type. If we implement a ShapeMaker class that provides a make() method that returns a Shape, we can override it in a derived class MakeTriangle to return Triangle instead, as shown in the following listing. The compiler allows this, as calling either of the make() methods will give us a Shape object.

清单 7.20。覆盖一个子类型作为返回类型的方法
类 ShapeMaker {
    制作():形状{                         1
        返回新形状();
    }
}

类 TriangleMaker 扩展 ShapeMaker {    2 
    make(): 三角形 {                      3
        返回新三角形();
    }
}
class ShapeMaker {
    make(): Shape {                        1
        return new Shape();
    }
}

class TriangleMaker extends ShapeMaker {   2
    make(): Triangle {                     3
        return new Triangle();
    }
}

  • 1 ShapeMaker 定义了一个 make() 方法,它返回一个 Shape 对象。
  • 1 ShapeMaker defines a method make(), which returns a Shape object.
  • 2 TriangleMaker 继承自ShapeMaker。
  • 2 TriangleMaker inherits from ShapeMaker.
  • 3 TriangleMaker 重写 make() 并将其返回类型更改为 Triangle。
  • 3 TriangleMaker overrides make() and changes its return type to Triangle.

同样,大多数主流编程语言都允许这种行为,因为大多数人认为函数的返回类型是协变的。让我们看看参数类型是彼此的子类型的函数类型会发生什么。

Again, this behavior is allowed in most mainstream programming languages, as most consider functions to be covariant in their return type. Let’s see what happens to function types whose argument types are subtypes of one another.

7.3.4.子类型和函数参数类型

7.3.4. Subtyping and function argument types

我们将在这里把事情翻个底朝天,所以我们不会使用一个返回 a 的函数Shape和一个返回 a 的函数Triangle,而是使用一个以 aShape作为参数的函数和一个以 a 作为Triangle参数的函数。我们将调用这些函数drawShape()drawTriangle()。如何相互关联 (argument: Shape) => void(argument: Triangle) => void

We’ll turn things inside out here, so instead of using a function that returns a Shape and a function that returns a Triangle, we’ll take a function that takes a Shape as argument and a function that takes a Triangle as argument. We’ll call these functions drawShape() and drawTriangle(). How do (argument: Shape) => void and (argument: Triangle) => void relate to each other?

让我们介绍另一个函数 ,它将 a和一个函数render()作为参数,如下一个清单所示。它只是用给定的调用给定的函数。 Triangle(argument: Triangle) => voidTriangle

Let’s introduce another function, render(), that takes as arguments a Triangle and an (argument: Triangle) => void function, as the next listing shows. It simply calls the given function with the given Triangle.

清单 7.21。绘制和渲染函数
声明函数 drawShape(shape: Shape): void;            1
声明函数 drawTriangle(triangle: Triangle): void;   1个

函数渲染(
    triangle: Triangle,                                     2 
    drawFunc: (argument: Triangle) => void): void {         2 
    drawFunc(triangle);                                    3 
}
declare function drawShape(shape: Shape): void;            1
declare function drawTriangle(triangle: Triangle): void;   1

function render(
    triangle: Triangle,                                    2
    drawFunc: (argument: Triangle) => void): void {        2
    drawFunc(triangle);                                    3
}

  • 1 drawShape() 接受一个 Shape 参数;drawTriangle() 采用 Triangle 参数。
  • 1 drawShape() takes a Shape argument; drawTriangle() takes a Triangle argument.
  • 2 render() 需要一个三角形和一个以三角形作为参数的函数。
  • 2 render() expects a Triangle and a function that takes a Triangle as argument.
  • 3 render() 简单地调用提供的函数,将它接收到的三角形传递给它。
  • 3 render() simply calls the provided function, passing it the triangle it received.

有趣的是:在这种情况下,我们可以安全地传递drawShape()render()函数!我们可以在需要 (argument: Shape) => voidan 的地方使用 a 。(argument: Triangle) => void

Here comes the interesting bit: in this case, we can safely pass drawShape() to the render() function! We can use a (argument: Shape) => void where an (argument: Triangle) => void is expected.

从逻辑上讲,这是有道理的:我们有一个Triangle,我们将它传递给可以将其用作参数的绘图函数。如果函数本身需要一个Triangle,就像我们的drawTriangle()函数一样,它当然可以工作。但它也应该适用于期望超类型Triangle. drawShape()想要一个形状——任何形状——来画。因为它不使用任何特定于三角形的东西,所以它比drawTriangle(); 它可以接受任何形状作为参数,无论是 itTriangle还是Square. 所以在这种特殊情况下,子类型关系是相反的。

Logically, it makes sense: we have a Triangle, and we pass it to a drawing function that can use it as an argument. If the function itself expects a Triangle, like our drawTriangle() function, it of course works. But it should also work for a function that expects a supertype of Triangle. drawShape() wants a shape—any shape—to draw. Because it doesn’t use anything that’s triangle-specific, it is more general than drawTriangle(); it can accept any shape as argument, be it Triangle or Square. So in this particular case, the subtyping relationship is reversed.

逆变

反转其基础类型的子类型化关系的类型称为逆变。在大多数编程语言中,函数在参数方面是逆变的。期望 as 参数的函数Triangle可以替换期望Shapeas 参数的函数。函数的关系与参数类型的关系相反。如果Triangle是 的子类型Shape,则以 a 作为参数的函数类型是以 a作为参数Triangle的函数类型的超类型(图 7.6)。 Shape

A type that reverses the subtyping relationship of its underlying type is called contravariant. In most programming languages, functions are contravariant with regard to their arguments. A function that expects a Triangle as argument can be substituted for a function that expects a Shape as argument. The relationship of the functions is the reverse of the relationship of the argument types. If Triangle is a subtype of Shape, the type of function that takes a Triangle as an argument is a supertype of the type of function that takes a Shape as an argument (figure 7.6).

图 7.6。如果Triangle是 的子类型Shape,我们可以使用期望 a 作为参数的函数而不是期望 a作为参数的Shape函数,因为我们总是可以将 a 传递给接受 a 的函数。 TriangleTriangleShape

我们之前说过“大多数编程语言”。一个值得注意的例外是 TypeScript。在 TypeScript 中,我们也可以做相反的事情:传递一个需要子类型的函数,而不是传递一个需要超类型的函数。这个选择是为了促进常见的 JavaScript 编程模式而做出的明确设计选择。但是,它可能会导致运行时问题。

We said “most programming languages” earlier. A notable exception is TypeScript. In TypeScript, we can also do the opposite: pass a function that expects a subtype instead of a function that expects a supertype. This choice was an explicit design choice made to facilitate common JavaScript programming patterns. It can lead to run-time issues, though.

让我们看一下下一个清单中的示例。首先,我们将isRightAngled()在我们的Triangle类型上定义一个方法,该方法将确定给定实例是否描述直角三角形。方法的实现并不重要。

Let’s look at an example in the next listing. First, we’ll define a method isRightAngled() on our Triangle type, which would determine whether a given instance describes a right-angled triangle. The implementation of the method is not important.

清单 7.22。Shape和方法 Triangle_isRightAngled()
类形状{
    /* 形状成员 */
}

声明 const TriangleType:唯一符号;
类三角形扩展形状{
    [三角形类型]: void;

    isRightAngled(): boolean {               1
        让结果: boolean = false;

        /* 判断是否为直角三角形 */

        返回结果;
    }

    /* 更多三角形成员 */
}
class Shape {
    /* Shape members */
}

declare const TriangleType: unique symbol;
class Triangle extends Shape {
    [TriangleType]: void;

    isRightAngled(): boolean {              1
        let result: boolean = false;

        /* Determine whether it is a right-angled triangle */

        return result;
    }

    /* More Triangle members */
}

  • 1 isRightAngled() 方法告诉我们实例是否描述直角三角形。
  • 1 The isRightAngled() method tells us whether an instance describes a right-angled triangle.

现在让我们反转绘图示例,如代码清单 7.23所示。假设我们的render()函数需要 aShape而不是 aTriangle和可以绘制形状的函数(argument: Shape) => void而不是只能绘制三角形的函数(argument: Triangle) => void

Now let’s reverse the drawing example, as shown in listing 7.23. Suppose that our render() function expects a Shape instead of a Triangle and a function that can draw shapes (argument: Shape) => void instead of a function that can draw only triangles (argument: Triangle) => void.

清单 7.23。更新了绘制和渲染函数
声明函数 drawShape(shape: Shape): void;             1
声明函数 drawTriangle(triangle: Triangle): void;    1个

函数渲染(
    shape: Shape ,                                            2 
    drawFunc: (argument: Shape ) => void): void {             2 
    drawFunc(shape);                                        3 
}
declare function drawShape(shape: Shape): void;             1
declare function drawTriangle(triangle: Triangle): void;    1

function render(
    shape: Shape,                                           2
    drawFunc: (argument: Shape) => void): void {            2
    drawFunc(shape);                                        3
}

  • 1 drawShape() 和 drawTriangle() 就像以前一样。
  • 1 drawShape() and drawTriangle() are just like before.
  • 2 render() 需要一个 Shape 和一个以 Shape 作为参数的函数。
  • 2 render() expects a Shape and a function that takes a Shape as argument.
  • 3 render() 简单地调用提供的函数,将它接收到的形状传递给它。
  • 3 render() simply calls the provided function passing it the shape it received.

以下是我们如何导致运行时错误:我们可以定义drawTriangle()使用特定于三角形的东西,例如isRightAngled()我们刚刚添加的方法。然后我们用Shape对象(不是Triangle)和调用 render drawTriangle()

Here’s how we can cause a run-time error: we can define drawTriangle() to use something that is triangle-specific, such as the isRightAngled() method we just added. Then we call render with a Shape object (not a Triangle) and drawTriangle().

NowdrawTriangle()将接收一个对象并尝试在下一个清单中Shape调用它,但是因为不是,这将导致错误。 isRight-Angled()ShapeTriangle

Now drawTriangle() will receive a Shape object and attempt to call isRight-Angled() on it in the next listing, but because the Shape is not a Triangle, this will cause an error.

清单 7.24。尝试调用isRightAngled()超类型Triangle
函数 drawTriangle(三角形:三角形):void {
    console.log( triangle.isRightAngled() );          1个
    /* ... */
}

函数渲染(
    形状:形状,
    drawFunc:(参数:Shape)=> void):void {
    drawFunc(形状);
}

渲染(新形状(),drawTriangle);                  2个
function drawTriangle(triangle: Triangle): void {
    console.log(triangle.isRightAngled());          1
    /* ... */
}

function render(
    shape: Shape,
    drawFunc: (argument: Shape) => void): void {
    drawFunc(shape);
}

render(new Shape(), drawTriangle);                  2

  • 1 drawTriangle() 在给定参数上调用特定于三角形的方法。
  • 1 drawTriangle() calls a Triangle-specific method on the given argument.
  • 2 我们可以通过一个Shape和drawTriangle()来渲染。
  • 2 We can pass a Shape and drawTriangle() to render.

这段代码可以编译,但它会在运行时失败并出现 JavaScript 错误,因为运行时将无法在我们提供给的对象isRightAngled()上找到。这个结果并不理想,但如前所述,这是在 TypeScript 实现过程中做出的有意识的决定。 ShapedrawTriangle()

This code will compile, but it will fail at run time with a JavaScript error, because the run time won’t be able to find isRightAngled() on the Shape object we gave to drawTriangle(). This result is not ideal, but as mentioned before, it was a conscious decision made during the implementation of TypeScript.

在 TypeScript 中,如果Triangle是 的子类型Shape,则类型的函数(argument: Shape) => void和类型的函数(argument: Triangle) => void可以相互替代。实际上,它们是彼此的子类型。此属性称为双方差

In TypeScript, if Triangle is a subtype of Shape, a function of type (argument: Shape) => void and a function of type (argument: Triangle) => void can be substituted for each other. Effectively, they are subtypes of each other. This property is called bivariance.

双方差

如果从它们的底层类型的子类型化关系来看,它们成为彼此的子类型,则类型是双变的。在 TypeScript 中,如果Triangle是 的子类型Shape,则函数类型(argument: Shape) => void和 (argument: Triangle) => void是彼此的子类型(图 7.7)。

Types are bivariant if, from the subtyping relationship of their underlying types, they become subtypes of each other. In TypeScript, if Triangle is a subtype of Shape, the function types (argument: Shape) => void and (argument: Triangle) => void are subtypes of each other (figure 7.7).

图 7.7。如果Triangle是 的子类型Shape,在 TypeScript 中,可以使用期望 a 的函数Triangle代替期望 a 的函数Shape,并且可以使用期望 a 的函数Shape代替期望 a 的函数Triangle

同样,函数在 TypeScript 中关于其参数的双变性允许编译不正确的代码。本书的一个主题是依靠类型系统来消除编译时的运行时错误。在 TypeScript 中,启用通用 JavaScript 编程模式是一项深思熟虑的设计决策。

Again, the bivariance of functions with respect to their arguments in TypeScript allows incorrect code to compile. A major theme of this book is relying on the type system to eliminate run-time errors at compile time. In TypeScript, it was a deliberate design decision to enable common JavaScript programming patterns.

7.3.5.方差回顾

7.3.5. Variance recap

在本节中,我们研究了哪些类型可以替代哪些其他类型。尽管子类型化对于处理简单的继承来说很简单,但是当我们添加在其他类型上参数化的类型时,事情会变得更加复杂。这些类型可以是集合、函数类型或其他泛型类型。这些参数化类型的子类型化关系被移除、保留、反转或根据其底层类型的关系制成双向的方式称为方差:

Throughout this section, we’ve looked at what types can be substituted for what other types. Although subtyping is straightforward for dealing with simple inheritance, things get more complicated when we add types parameterized on other types. These types could be collections, function types, or other generic types. The way that the subtyping relationships of these parameterized types is removed, preserved, reversed, or made two-way based on the relationship of their underlying types is called variance:

  • 不变类型忽略其基础类型的子类型化关系。
  • Invariant types ignore the subtyping relationship of their underlying types.
  • 协变类型保留其基础类型的子类型关系。如果Triangle是 的子类型Shape,则类型数组Triangle[]是类型数组的子类型Shape[]。在大多数编程语言中,函数类型的返回类型是协变的。
  • Covariant types preserve the subtyping relationship of their underlying types. If Triangle is a subtype of Shape, an array of type Triangle[] is a subtype of an array of type Shape[]. In most programming languages, function types are covariant in their return types.
  • 逆变类型反转其基础类型的子类型关系。如果Triangle是 的子类型Shape,则函数类型是大多数语言中(argument: Shape) => void函数类型的子类型。(argument: Triangle) => void这对于 TypeScript 来说并非如此,其中函数类型在其参数类型方面是双变的。
  • Contravariant types reverse the subtyping relationship of their underlying types. If Triangle is a subtype of Shape, the function type (argument: Shape) => void is a subtype of the function type (argument: Triangle) => void in most languages. This is not true for TypeScript, in which function types are bivariant with regard to their argument types.
  • 当它们的基础类型处于子类型化关系中时,双变类型是彼此的子类型。如果Triangle是 的子类型Shape,则函数类型(argument: Shape) => void和函数类型(argument: Triangle) => void互为子类型。(两种类型的功能可以相互替代。)
  • Bivariant types are subtypes of each other when their underlying types are in a subtyping relationship. If Triangle is a subtype of Shape, the function type (argument: Shape) => void and the function type (argument: Triangle) => void are subtypes of each other. (Functions of both types can be substituted for each other.)

尽管编程语言之间存在一些通用规则,但没有一种方法可以支持差异。您应该了解您的编程语言的类型系统的作用以及它如何建立子类型关系。知道这一点很重要,因为这些规则告诉我们什么可以替代什么。您是否需要实现将 a 转换List<Triangle>为 a 的函数List<Shape>,还是可以按List<Triangle>原样使用?答案取决于List<T>您选择的编程语言的差异。

Although some common rules exist across programming languages, there is no one way to support variance. You should understand what the type system of your programming language does and how it establishes subtyping relationships. This is important to know, as these rules tell us what can be substituted for what. Do you need to implement a function to transform a List<Triangle> into a List<Shape>, or can you just use the List<Triangle> as is? The answer depends on the variance of List<T> in your programming language of choice.

7.3.6.练习

7.3.6. Exercises

在下面的练习中,Triangle是 的子类型Shape。我们将使用 TypeScript 的方差规则。

In the following exercises, Triangle is a subtype of Shape. We are going to use the variance rules of TypeScript.

1个

我们可以将Triangle变量传递给函数吗drawShape(shape: Shape): void

1

Can we pass a Triangle variable to a function drawShape(shape: Shape): void?

2个

我们可以将Shape变量传递给函数吗drawTriangle(triangle: Triangle): void

2

Can we pass a Shape variable to a function drawTriangle(triangle: Triangle): void?

3个

我们可以将对象数组Triangle( Triangle[]) 传递给函数吗drawShapes(shapes: Shape[]): void

3

Can we pass an array of Triangle objects (Triangle[]) to a function drawShapes(shapes: Shape[]): void?

4个

我们可以将drawShape()函数分配给函数类型的变量(triangle: Triangle) => void吗?

4

Can we assign the drawShape() function to a variable of function type (triangle: Triangle) => void?

5个

我们可以将drawTriangle()函数分配给函数类型的变量(shape: Shape) => void吗?

5

Can we assign the drawTriangle() function to a variable of function type (shape: Shape) => void?

6个

我们可以将函数分配getShape(): Shape给函数的变量type () => Triangle吗?

6

Can we assign a function getShape(): Shape to a variable of function type () => Triangle?

概括

Summary

  • 我们定义了子类型以及编程语言确定一种类型是否是另一种类型的子类型的两种方式:结构的或名义的。
  • We defined subtyping and the two ways that programming languages determine whether a type is a subtype of another type: structural or nominal.
  • 我们研究了一种 TypeScript 技术来模拟具有结构子类型的语言中的名义子类型。
  • We looked at a TypeScript technique to simulate nominal subtyping in a language with structural subtyping.
  • 我们看到了顶级类型的应用程序,该类型位于子类型层次结构的顶部:安全反序列化。
  • We saw an application for the top type, the type that sits at the top of the subtyping hierarchy: safe deserialization.
  • 我们还看到了底层类型的应用,位于子类型层次结构底部的类型:作为错误场景的值类型。
  • We also saw an application for the bottom type, the type that sits at the bottom of the subtyping hierarchy: as a value type for error scenarios.
  • 我们介绍了求和类型之间的子类型化。由较少类型组成的和类型是由较多类型组成的和类型的超类型。
  • We covered subtyping between sum types. The sum type composed of fewer types is the supertype of the sum type composed of more types.
  • 我们了解了协变类型。数组和集合通常是协变的,函数类型的返回类型也是协变的。
  • We learned about covariant types. Arrays and collections are often covariant, and function types are covariant in their return types.
  • 在某些语言中,类型可以是不变的(没有子类型关系),即使它们的底层类型具有子类型关系。
  • In some languages, types can be invariant (have no subtyping relationship) even if their underlying types have a subtyping relationship.
  • 函数类型的参数类型通常是逆变的。换句话说,它们的子类型关系与其参数类型的关系相反。
  • Function types are usually contravariant in their argument types. In other words, their subtyping relationship is the reverse of that of their argument types.
  • 在 TypeScript 中,函数的参数类型是双变的。只要它们的参数类型具有子类型关系,每个函数类型都是另一个的子类型。
  • In TypeScript, functions are bivariant in their argument types. As long as their argument types have a subtyping relationship, each function type is a subtype of the other.
  • 差异在不同的编程语言中以不同的方式实现。最好了解您选择的编程语言如何建立子类型关系。
  • Variance is implemented differently in different programming languages. It’s good to know how your programming language of choice establishes subtyping relationships.

既然我们已经详细介绍了子类型,我们将继续讨论我们没有过多讨论的子类型的一个主要应用:面向对象编程。在第 8 章中,我们将回顾 OOP 的元素及其应用。

Now that we’ve covered subtyping at length, we’ll move on to the one major application of subtyping we haven’t talked about much: object-oriented programming. In chapter 8, we will go over the elements of OOP and their applications.

习题答案

Answers to exercises

区分 TypeScript 中的相似类型

Distinguishing between similar types in TypeScript

1个

是的——Painting与 具有相同的形状Wine,但有一个额外的painter属性。在 TypeScript 中,由于结构子类型化,PaintingWine.

1

Yes—Painting has the same shape as Wine, with an additional painter property. In TypeScript, due to structural subtyping, Painting is a subtype of Wine.

2个

No—Car缺少定义name的属性Wine,因此即使使用结构子类型,也Car不能替代Wine

2

No—Car is missing the name property that Wine defines, so even with structural subtyping, Car cannot be substituted for Wine.

 

 

分配给任何东西,分配给任何东西

Assigning anything to, assigning to anything

1个

Yes—never是任何其他类型的子类型,包括number,所以我们可以将它分配给一个数字(即使我们永远无法创建实际值,因为makeNothing()永远不会返回)。

1

Yes—never is a subtype of any other type, including number, so we can assign it to a number (even though we would never be able to create an actual value, as makeNothing() would never return).

2个

No—unknown是任何其他类型的超类型,包括number. 我们可以将 a 分配number给 an unknown,但反之则不行。首先,我们必须确保返回的值makeSomething()是一个数字,然后才能将其分配给x.

2

No—unknown is a supertype of any other type, including number. We can assign a number to an unknown, but not vice versa. First, we have to ensure that the value returned from makeSomething() is a number before we can assign it to x.

 

 

允许替换

Allowed substitutions

1个

是的——我们可以在需要 Trianglea 的地方替换 a。Shape

1

Yes—We can substitute a Triangle wherever a Shape is expected.

2个

否——我们不能使用超类型代替子类型。

2

No—We cannot use a supertype instead of a subtype.

3个

是的——数组是协变的,所以我们可以使用对象数组Triangle而不是Shape对象数组。

3

Yes—Arrays are covariant, so we can use an array of Triangle objects instead of an array of Shape objects.

4个

是的——函数在 TypeScript 中的参数是双变的,所以我们可以使用(shape: Shape) => voidas (triangle: Triangle) => void

4

Yes—Functions are bivariant in their arguments in TypeScript, so we can use (shape: Shape) => void as (triangle: Triangle) => void.

5个

是的——函数在 TypeScript 中的参数是双变的,所以我们可以使用(triangle: Triangle) => voidas (shape: Shape) => void

5

Yes—Functions are bivariant in their arguments in TypeScript, so we can use (triangle: Triangle) => void as (shape: Shape) => void.

6个

否——在 TypeScript 中,函数的参数是双变的,但返回类型不是。我们不能将 type 的函数用作 () => Shapetype 的函数() => Triangle

6

No—Functions are bivariant in their arguments but not in their return types in TypeScript. We can’t use a function of type () => Shape as a function of type () => Triangle.

 

 

第 8 章。面向对象编程的要素

Chapter 8. Elements of object-oriented programming

本章涵盖

This chapter covers

  • 使用接口定义契约
  • Defining contracts by using interfaces
  • 实现表达式的层次结构
  • Implementing a hierarchy of expressions
  • 实施适配器模式
  • Implementing the adapter pattern
  • 使用混合扩展行为
  • Extending behavior with mix-ins
  • 考虑纯 OOP 的替代方案
  • Considering alternatives to pure OOP

在本章中,我们将介绍面向对象编程的元素,并了解如何有效地使用它们。您可能熟悉这些概念,因为它们出现在所有面向对象的语言中,因此我们将更多地关注它们的用例。

In this chapter, we will cover the elements of object-oriented programming and see how we can employ them effectively. You are probably familiar with these concepts, as they show up in all object-oriented languages, so we’ll focus more on their use cases.

我们将从接口开始,看看我们如何将它们视为契约。在接口之后,我们将看看继承:我们可以继承数据和行为。继承的替代方法是组合。我们将研究这两种方法之间的一些差异以及何时使用哪种方法。我们将讨论使用混合或 TypeScript 中的交集类型来扩展数据和行为。并非所有语言都支持混入。最后,我们将研究 OOP 的替代方案以及何时不使用它可能有意义。这并不是因为 OOP 有什么问题,而是因为许多开发人员将其作为软件工程的唯一方法来学习,有时它最终会被过度使用。

We’ll start with interfaces and see how we can think of them as contracts. After interfaces, we’ll look at inheritance: we can inherit both data and behavior. An alternative to inheritance is composition. We’ll look at some of the differences between the two approaches and when to use which. We’ll talk about extending data and behavior with mix-ins or, in TypeScript, intersection types. Not all languages support mix-ins. Finally, we’ll look at alternatives to OOP and when it might make sense not to use it. This is not because there is something wrong with OOP, but because many developers learn it as the only approach to software engineering, and sometimes it ends up being overused.

在开始之前,让我们快速定义一下 OOP。

Before getting started, let’s quickly define OOP.

面向对象编程

OOP 是一种基于对象概念的编程范式,它同时包含数据和代码。数据是对象的状态。代码是一种或多种方法,也称为消息。在面向对象的系统中,对象可以通过调用彼此的方法来相互“交谈”或发送消息。

OOP is a programming paradigm based on the concept of objects, which contain both data and code. The data is the state of the object. The code is one or more methods, also known as messages. In an object-oriented system, objects can “talk” to or message one another by invoking each other’s methods.

OOP 的两个关键特性是封装,它允许我们隐藏数据和方法,以及继承,它使用额外的数据和/或代码扩展类型。

Two key features of OOP are encapsulation, which allows us to hide data and methods, and inheritance, which extends a type with additional data and/or code.

8.1. 使用接口定义合约

8.1. Defining contracts with interfaces

在本节中,我们将尝试回答一个常见的 OOP 问题:抽象类和接口之间有什么区别?让我们以日志系统为例。我们想提供一种log()方法,但仍然能够使用不同的日志记录实现。我们可以通过几种方式解决这个问题。首先,我们可以声明一个抽象类,ALogger并让实际的实现(例如)Console-Logger从它继承,如以下清单所示。

In this section, we’ll try to answer a common OOP question: what is the difference between an abstract class and an interface? Let’s take as an example a logging system. We want to provide a log() method but still have the ability to use different logging implementations. We can go about this in a couple of ways. First, we can declare an abstract class, ALogger, and have the actual implementations, such as Console-Logger, inherit from it, as shown in the following listing.

清单 8.1。抽象记录器
抽象类 ALogger {                  1
    抽象日志(行:字符串):无效;    2个
}

类 ConsoleLogger 扩展 ALogger {     3
    日志(行:字符串):void {             3
        控制台日志(行);
    }
}
abstract class ALogger {                 1
    abstract log(line: string): void;    2
}

class ConsoleLogger extends ALogger {    3
    log(line: string): void {            3
        console.log(line);
    }
}

  • 1 ALogger 是一个抽象类。
  • 1 ALogger is an abstract class.
  • 2 log() 是一个抽象方法,缺乏实现。
  • 2 log() is an abstract method, lacking implementation.
  • 3 ConsoleLogger继承自ALogger,提供了log()的实现。
  • 3 ConsoleLogger inherits from ALogger and provides an implementation for log().

日志系统的用户会将 anALogger作为参数。我们可以在需要 an 的任何地方传递 的任何子类型ALogger,例如。ConsoleLogerALogger

A user of the logging system would take an ALogger as a parameter. We can pass any subtype of ALogger, such as ConsoleLoger, anywhere that an ALogger is expected.

另一种方法是声明一个ILogger接口并ConsoleLogger实现该接口,如下一个清单所示。

The alternative is to declare an ILogger interface and have ConsoleLogger implement that interface, as shown in the next listing.

清单 8.2。记录器接口
接口 ILogger {                          1
    日志(行:字符串):无效;
}

类 ConsoleLogger 实现 ILogger {     2 
    log(line: string): void {                2
        控制台日志(行);
    }
}
interface ILogger {                         1
    log(line: string): void;
}

class ConsoleLogger implements ILogger {    2
    log(line: string): void {               2
        console.log(line);
    }
}

  • 1 ILogger 接口声明了一个 log() 方法。
  • 1 ILogger interface declares a log() method.
  • 2 ConsoleLogger实现了ILogger接口,提供了一个log()方法。
  • 2 ConsoleLogger implements ILogger interface and provides a log() method.

在这种情况下,日志系统的用户将采用 anILogger作为参数。我们可以传递实现接口的任何类型,例如ConsoleLogger,任何ILogger需要 的地方。

A user of the logging system would, in this case, take an ILogger as a parameter. We can pass any type implementing the interface, such as ConsoleLogger, anywhere that an ILogger is expected.

这两种方法很相似,而且都有效,但是在这种情况下,我们应该使用接口,因为接口指定了契约

The two approaches are similar, and both work, but in a scenario like this one, we should use an interface because an interface specifies a contract.

接口或契约

接口或契约是对一组消息的描述,实现该接口的任何对象都可以理解这些消息消息是方法,包括名称、参数和返回类型。接口没有任何状态。就像现实世界的合同一样,它是书面协议,接口是实施者将提供的书面协议。

An interface, or a contract, is a description of a set of messages that are understood by any object implementing that interface. The messages are methods and include name, arguments, and return type. An interface does not have any state. Just like real-world contracts, which are written agreements, an interface is a written agreement of what implementers will provide.

log()这正是我们在本例中所需要的:由客户端将调用的方法组成的日志记录合约。声明接口ILogger可以让阅读我们代码的人清楚地知道我们正在指定一个合同。

This is exactly what we need in our case: the logging contract consisting of a log() method that clients will call. Declaring the ILogger interface makes it clear to whoever reads our code that we are specifying a contract.

抽象类可以做到这一点,但它可以做的更多:它可以包含非抽象方法或状态。抽象类与“普通”类或具体类之间的唯一区别是我们不能直接创建抽象类的实例。我们知道,每当我们传递抽象类的实例(例如参数)时ALogger,我们实际上是在处理从 继承的类型的实例ALogger,例如ConsoleLogger

An abstract class can do that, but it can do much more: it can contain nonabstract methods or state. The only difference between an abstract and a “normal” or concrete class is that we can’t directly create an instance of an abstract class. We know that whenever we pass around an instance of the abstract class, such as an ALogger argument, we are in fact working with an instance of a type that inherits from ALogger, such as ConsoleLogger.

ConsoleLogger这是抽象类和接口之间微妙但重要的区别:和之间的关系ALogger称为is-a 关系,因为它继承自它ConsoleLoggerALogger另一方面,没有什么可以继承的ILogger,因为它只是指定了一个契约。我们已经ConsoleLogger实现了契约,但它并没有在语义上创建一个is-a关系。满足ConsoleLogger 合同 ILogger不是. ILogger这就是为什么即使强制一个类只能从另一个类继承的语言(例如 Java 和 C#)仍然允许类实现许多接口的原因。

This is a subtle but important distinction between abstract classes and interfaces: the relationship between ConsoleLogger and ALogger is called an is-a relationship, as in ConsoleLogger is an ALogger, because it inherits from it. On the other hand, there is nothing to inherit from ILogger, as it just specifies a contract. We have ConsoleLogger implement the contract, but it doesn’t semantically create an is-a relationship. ConsoleLogger satisfies the contract ILogger but isn’t an ILogger. That’s the reason why even languages that enforce that a class can inherit from only one other class, such as Java and C#, still allow classes to implement many interfaces.

请注意,我们可以扩展一个接口,基于它创建一个新接口,并使用其他方法。我们可以创建一个向合约IExtendedLogger添加warn()一个方法的方法,例如,如以下清单所示, error()ILogger

Note that we can extend an interface, creating a new interface based on it, with additional methods. We can create an IExtendedLogger that adds a warn() and an error() method to the ILogger contract, for example, as the following listing shows,

清单 8.3。扩展记录器接口
接口 ILogger {
    日志(行:字符串):无效;
}

接口 IExtendedLogger 扩展 ILogger {       1
    警告(行:字符串):无效;
    错误(行:字符串):无效;
}
interface ILogger {
    log(line: string): void;
}

interface IExtendedLogger extends ILogger {      1
    warn(line: string): void;
    error(line: string): void;
}

  • 1 IExtendedLogger 具有 log()、warn() 和 error() 方法。
  • 1 IExtendedLogger has log(), warn(), and error() methods.

满足IExtendedLogger契约的任何对象也自动满足ILogger契约。我们也可以将多个接口合二为一。我们可以以 anISpeaker和 anIVolumeControl为例,定义一个ISpeakerWithVolumeControl结合两者的合约,如代码清单 8.4所示。这种技术允许我们将扬声器功能和音量控制功能用作合同,同时仍然允许其他类型仅实现其中之一。(例如,我们可能对麦克风进行音量控制。)

Any object that satisfies the IExtendedLogger contract also satisfies the ILogger contract automatically. We can also combine multiple interfaces into one. We can take an ISpeaker and an IVolumeControl, for example, and define an ISpeakerWithVolumeControl contract that combines the two, as shown in listing 8.4. This technique allows us to use as a contract both the speaker capabilities and the volume-control capabilities while still allowing other types to implement only one of them. (We might have volume control for a microphone, for example.)

清单 8.4。组合接口
接口 ISpeaker {                                                      1
    播放声音(/* ... */):无效;
}

接口 IVolumeControl {                                                2
    音量上升():无效;
    音量下降():无效;
}

接口 ISpeakerWithVolumeControl 扩展 ISpeaker, IVolumeControl {
}                                                                         3

类 MySpeaker 实现 ISpeakerWithVolumeControl {                    4
    播放声音(/* ... */):无效{
        // 具体实现
    }

    volumeUp(): void {
        // 具体实现
    }

    音量下降():无效{
        // 具体实现
    }
}

类音乐播放器{
    扬声器:ISpeakerWithVolumeControl;                                  5个

    构造函数(扬声器:ISpeakerWithVolumeControl){
        this.speaker = 扬声器;
    }
}
interface ISpeaker {                                                     1
    playSound(/* ... */): void;
}

interface IVolumeControl {                                               2
    volumeUp(): void;
    volumeDown(): void;
}

interface ISpeakerWithVolumeControl extends ISpeaker, IVolumeControl {
}                                                                        3

class MySpeaker implements ISpeakerWithVolumeControl {                   4
    playSound(/* ... */): void {
        // Concrete implementation
    }

    volumeUp(): void {
        // Concrete implementation
    }

    volumeDown(): void {
        // Concrete implementation
    }
}

class MusicPlayer {
    speaker: ISpeakerWithVolumeControl;                                  5

    constructor(speaker: ISpeakerWithVolumeControl) {
        this.speaker = speaker;
    }
}

  • 1个 音箱接口
  • 1 Speaker interface
  • 2 音量控制界面
  • 2 Volume-control interface
  • 3 组合扬声器和音量控制接口
  • 3 Combined speaker and volume-control interface
  • 4 MySpeaker 实现组合界面
  • 4 MySpeaker implementing the combined interface
  • 5 MusicPlayer 需要一个带音量控制的扬声器。
  • 5 MusicPlayer requires a speaker with volume controls.

当然,我们可以MySpeaker同时实现ISpeakerIVolumeControl而不是,但是使用单一接口可以使组件更容易请求具有音量控制的扬声器。像这样组合界面的能力使我们能够从更小的、可重用的构建块中创建它们。 ISpeakerWithVolumeControlMusicPlayer

We can have MySpeaker implement both ISpeaker and IVolumeControl instead of ISpeakerWithVolumeControl, of course, but using a single interface makes it easier for a component such as MusicPlayer to request a speaker with volume controls. The ability to combine interfaces like this allows us to create them from smaller, reusable building blocks.

接口最终有利于消费者,而不是实现它们的类,所以花一些时间想出最好的设计通常是个好主意。众所周知的针对接口编码的 OOP 原则鼓励使用接口而不是类,正如我们MusicPlayer在示例中所做的那样。该原则减少了系统中组件的耦合,因为我们可以修改甚至换出MySpeaker另一种类型而不会影响MusicPlayer,只要ISpeakerWithVolumeContract满足 即可。

Interfaces ultimately benefit the consumers, not the classes that implement them, so it’s generally a good idea to spend some time coming up with the best design. The well-known OOP principle of coding against interfaces encourages working with interfaces rather than classes, as we did with MusicPlayer in our example. That principle reduces the coupling of the components in the system, as we can modify or even swap out MySpeaker for another type without affecting MusicPlayer, as long as the ISpeakerWithVolumeContract is satisfied.

依赖注入框架负责映射我们应该用于该接口的具体实现,因此其余代码只是请求某个接口,而框架会提供它。这减少了“胶水”代码,使我们能够专注于实现组件本身。我们不会详细介绍依赖注入,但它是减少代码耦合的好方法,对单元测试特别有用,因为我们通常将被测组件的依赖设置为存根或模拟。

Dependency injection frameworks take on the responsibility of mapping the concrete implementation we should use for that interface, so the rest of the code simply asks for a certain interface, and the framework provides it. This reduces the “glue” code and allows us to focus on implementing the components themselves. We won’t cover dependency injection at length, but it’s a good approach to reducing the coupling of the code and especially useful for unit testing, as we usually set up dependencies of components under test to be stubs or mocks.

接下来,我们将研究继承及其一些应用。

Next, we’ll look at inheritance and some of its applications.

8.1.1.练习

8.1.1. Exercises

1个

getName()函数可以使用具有函数的类型的实例index()。对此建模的最佳方法是什么?

  1. 声明一个具体的BaseNamed基类
  2. 声明一个ANamed抽象基类
  3. 声明一个INamed接口
  4. 检查getName()运行时是否存在

1

Instances of types that have a getName() function can be used by an index() function. What is the best way to model this?

  1. Declare a concrete BaseNamed base class
  2. Declare an ANamed abstract base class
  3. Declare an INamed interface
  4. Check whether getName() exists at run time

2个

在 TypeScript 中,Iterable<T>接口声明了一个[Symbol.iterator]返回 an 的方法Iterator<T>Iterator<T>接口声明了一个next()返回 an 的方法IteratorResult<T>

接口可迭代<T> {
    [Symbol.iterator](): Iterator<T>;
}

接口迭代器<T> {
    下一个():迭代器结果<T>;
}

生成器返回这些的组合——一个IterableIterator<T>,它既是可迭代的又是迭代器本身。你将如何定义Iterable-Iterator<T>接口?

2

In TypeScript, the Iterable<T> interface declares a [Symbol.iterator] method that returns an Iterator<T>, and the Iterator<T> interfaces declares a next() method returning an IteratorResult<T>:

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

interface Iterator<T> {
    next(): IteratorResult<T>;
}

Generators return a combination of these—an IterableIterator<T>, which is both iterable and an iterator itself. How would you define the Iterable-Iterator<T> interface?

8.2. 继承数据和行为

8.2. Inheriting data and behavior

继承是面向对象语言最著名的特性之一。它允许我们创建父类的子类。子类继承父类的数据和方法。显然,子类是父类的子类型,因为只要需要父类,就可以始终使用子类的实例。

Inheritance is one of the best-known features of object-oriented languages. It allows us to create subclasses of a parent class. The subclasses inherit both the data and the methods of the parent class. A subclass is, obviously, a subtype of the parent class, as an instance of the subclass can always be used whenever the parent class is expected.

8.2.1.这是一个经验法则

8.2.1. The is-a rule of thumb

似乎有一个直接的应用程序:如果我们已经有一个实现了我们想要的大部分行为的类,我们可以继承它并添加缺少的东西。随意这样做的问题是双重的。首先,如果我们滥用继承,我们最终会得到非常难以理解和导航的深层类层次结构。其次,我们最终得到一个不一致的数据模型,其中的类没有意义。

There seems to be an immediate application: if we already have a class that implements most of the behavior we want, we can inherit from it and add what is missing. The problem with doing this haphazardly is twofold. First, if we abuse inheritance, we end up with deep hierarchies of classes that are very hard to understand and navigate. Second, we end up with an inconsistent data model in which the classes don’t make sense.

例如,如果我们有一个Point跟踪x和坐标类,我们可以从它继承一个并添加一个属性。我们可以通过圆心和半径来定义一个圆,并且已经可以表示圆心了。但这个定义应该让人觉得奇怪。 yCircleradiusPoint

If we have a Point class that tracks x and y coordinates, for example, we could inherit a Circle from it and add a radius property. We can define a circle by its center and radius, and Point can already represent the center. But this definition should feel odd.

清单 8.5。不良继承
类点{
    x:数字;
    y:数字;

    构造函数(x:数字,y:数字){
        这个.x = x;
        这个.y = y;
    }
}

类圆扩展点{        1
    半径:数字;

    构造函数(x:数字,y:数字,半径:数字){
        超级(x,y);
        this.radius = 半径;
    }
}
class Point {
    x: number;
    y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

class Circle extends Point {       1
    radius: number;

    constructor(x: number, y: number, radius: number) {
        super(x, y);
        this.radius = radius;
    }
}

  • 1 Circle 从 Point 继承其中心的 x 和 y 坐标。
  • 1 Circle inherits the x and y coordinates of its center from Point.

要理解为什么这感觉很奇怪,让我们看看我们建立的is-a关系。子类的实例在逻辑上是超类的实例吗?在这种情况下,不。一个 Circle不是一个Point。我们当然可以按照我们定义它的方式将其作为一个整体来使用,但似乎没有一个我们想要这样做的合理场景。

To understand why this feels odd, let’s look at the is-a relationship we established. Is an instance of the subclass logically an instance of the superclass? In this case, no. A Circle is not a Point. We can certainly use it as one, the way we defined it, but there doesn’t seem to be a reasonable scenario in which we would want to do that.

继承和is-a关系

继承在子类型与其父类型之间建立is-a关系。如果我们的基类是Shape,而我们的派生类是Circle,则关系是“Circle是一个Shape”。这是继承的语义,也是一个很好的测试,可以应用于两种类型,以确定我们是否应该使用继承。

Inheritance establishes an is-a relationship between the child type and its parent type. If our base class is Shape, and our derived class is Circle, the relationship is “Circle is a Shape.” This is the semantic meaning of inheritance and a good test to apply to two types to determine whether we should use inheritance.

我们将在 8.3 节中讨论另一种组合方法。在那之前,让我们看一下使用继承确实有意义的 几种情况。

We’ll go over the alternative approach of composition in section 8.3. Until then, let’s look at a few situations in which it does make sense to use inheritance.

8.2.2.层次结构建模

8.2.2. Modeling a hierarchy

我们应该考虑继承的一个例子是我们的数据模型是分层的。这个事实相当明显,所以我们不会详细介绍,但这是继承的最佳用途:当我们沿着继承链向下移动时,我们通过添加更多数据和/或更多行为来改进我们的类型(图8.1) .

One instance when we should look at inheritance is when our data model is hierarchical. This fact is fairly obvious, so we won’t cover it at length, but this is the best use of inheritance: as we move down the inheritance chain, we refine our types by adding more data and/or more behavior (figure 8.1).

图 8.1。所有的动物都吃。我们可以和宠物一起玩(但它们仍然需要吃饭)。猫也会喵喵叫(但它们仍然会玩耍和吃东西)。

图中的例子看似简单,却是对继承的完美运用。A Catis a Petis an Animal,随着我们深入层次结构,我们会得到更多的行为和状态。

The example in the figure may seem to be simplistic, but it is a perfect use of inheritance. A Cat is a Pet is an Animal, and as we go deeper down the hierarchy, we get more behavior and state.

当我们想要处理更高的抽象级别时,我们会在层次结构中往上走。如果我们只需要play()我们的动物,我们使用类型的参数Pet。如果我们需要特定的喵喵叫行为,我们使用类型的参数Cat

When we want to deal with a higher abstraction level, we go up the hierarchy. If we just need to play() with our animal, we use an argument of type Pet. If we need specific meowing behavior, we use an argument of type Cat.

这个例子应该非常简单,所以让我们继续一个更有趣的继承应用,它有一个转折点:不同的派生类以不同的方式实现某些行为。

This example should be very straightforward, so let’s move on to a more interesting application of inheritance, which has a twist: different derived classes implement some behavior differently.

8.2.3.表达式的参数化行为

8.2.3. Parameterizing behavior of expressions

我们应该使用继承的另一种情况是,当我们想要的大部分行为和状态对多种类型都是通用的,但其中一小部分需要在不同的实现中有所不同时。多种类型仍应通过我们的is-a测试。

The other situation in which we should use inheritance is when most of the behavior and state we want is common to multiple types, but a small part of it needs to vary across implementations. The multiple types should still pass our is-a test.

我们有一个可以计算为数字的表达式,我们有有两个操作数的二进制表达式,我们有求和和乘法表达式,我们通过对操作数进行加法和乘法来求值。

We have an expression that can be evaluated to a number, we have binary expressions that have two operands, and we have sum and multiply expressions that we evaluate by adding and multiplying the operands.

我们可以将表达式建模为IExpression带有eval()方法的接口。我们使它成为一个接口,因为它不持有任何状态。接下来,我们实现一个存储两个操作数的抽象类,如清单 8.6BinaryExpression所示,但我们保持抽象并让派生类实现它。每个都继承了两个操作数并提供了自己的实现(图 8.2)。 eval()Sum-ExpressionMulExpressionBinary-Expressioneval()

We can model an expression as an IExpression interface with an eval() method. We make it an interface because it doesn’t hold any state. Next, we implement a BinaryExpression abstract class that stores the two operands, as shown in listing 8.6, but we keep eval() abstract and let derived classes implement it. Sum-Expression and MulExpression each inherit the two operands from Binary-Expression and provide their own eval() implementation (figure 8.2).

图 8.2。BinaryExpression作为父项和SumExpression作为MulExpression子项 的表达式层次结构

清单 8.6。表达层次
接口 IExpression {                                      1
    评估():数字;
}

抽象类 BinaryExpression 实现 IExpression {     2
    只读一个:数字;
    只读 b:数字;

    构造函数(a:数字,b:数字){
        这个.a = a;
        这个.b = b;
    }

    抽象评估():数字;                                3个
}

类 SumExpression 扩展 BinaryExpression {               4
    评估():数字{
        返回 this.a + this.b;
    }
}

类 MulExpression 扩展 BinaryExpression {               4
    评估():数字{
        返回this.a * this.b;
    }
}
interface IExpression {                                     1
    eval(): number;
}

abstract class BinaryExpression implements IExpression {    2
    readonly a: number;
    readonly b: number;

    constructor(a: number, b: number) {
        this.a = a;
        this.b = b;
    }

    abstract eval(): number;                                3
}

class SumExpression extends BinaryExpression {              4
    eval(): number {
        return this.a + this.b;
    }
}

class MulExpression extends BinaryExpression {              4
    eval(): number {
        return this.a * this.b;
    }
}

  • 1 IExpression 不需要是一个类,因为它不保存状态。
  • 1 IExpression doesn’t need to be a class, as it doesn’t hold state.
  • 2 BinaryExpression 是一个存储两个操作数的类。
  • 2 BinaryExpression is a class storing the two operands.
  • 3 eval() 是抽象的,因为我们没有它的实现。
  • 3 eval() is abstract, as we don’t have an implementation for it.
  • 4 SumExpression 和MulExpression 都继承自BinaryExpression 并实现了eval()。
  • 4 Both SumExpression and MulExpression inherit from BinaryExpression and implement eval().

这应该通过我们的is-a测试:a SumExpressionis a BinaryExpression。当我们沿着层次结构往下走时,我们继承了公共部分(在我们的例子中,两个操作数)但是eval()为每个派生类参数化。

This should pass our is-a test: a SumExpression is a BinaryExpression. As we go down the hierarchy, we inherit the common parts (in our case, the two operands) but parameterize the eval() for each derived class.

需要注意的一件事是提出非常深的类层次结构,这使得代码更难导航,因为状态的各个部分和对象的方法来自层次结构中的不同级别。

One thing to watch out for is coming up with very deep hierarchies of classes, which makes the code harder to navigate, as various parts of the state and methods of an object come from different levels in the hierarchy.

通常,让子类成为具体的类而让层次结构中的所有父类都是抽象的也很好。这种技术可以更轻松地跟踪事物并避免意外行为。当子类重写父方法时可能会发生意外行为,但随后我们将其向上转换并将其作为父类型传递。这样一个对象的行为与父类的实例不同,这对于代码的维护者来说可能不直观。

Usually, it’s also good to have the children be concrete classes and all parents up the hierarchy be abstract. This technique makes it easier to keep track of things and avoid unexpected behavior. Unexpected behavior can happen when a child class overrides a parent method, but then we upcast it and pass it around as the parent type. Such an object would behave differently from an instance of the parent class, which might not be intuitive for maintainers of the code.

一些语言提供了一种方法来显式地将子类标记为不可继承的,以强制停止那里的层次结构。通常,这是使用诸如finalor之类的关键字来完成的sealed。我们应该尽可能多地使用它们。如果我们想覆盖或扩展行为,我们有一个更好的继承替代方案:组合。

Some languages provide a way to explicitly mark a child class as noninheritable to enforce stopping the hierarchy there. Usually, this is done with keywords such as final or sealed. We should use these as often as we can. If we want to override or extend behavior, we have a better alternative to inheritance: composition.

8.2.4.练习

8.2.4. Exercises

1个

以下哪项看起来像是对继承的良好使用?

  1. File延伸Folder
  2. Triangle延伸Point
  3. Parser延伸Compiler
  4. 以上都不是。

1

Which of the following looks like a good use of inheritance?

  1. File extends Folder.
  2. Triangle extends Point.
  3. Parser extends Compiler.
  4. None of the above.

2个

UnaryExpression使用具有单个操作数的 和UnaryMinusExpression切换其操作数符号的扩展本节中的示例。(例如,示例 1 变为 –1,而 –2 变为 2。)

2

Extend the example in this section with a UnaryExpression that has a single operand and a UnaryMinusExpression that toggles the sign of its operand. (Example 1 becomes –1, for example, and –2 becomes 2.)

8.3. 组合数据和行为

8.3. Composing data and behavior

面向对象编程的一个众所周知的原则是尽可能优先使用组合而不是继承。让我们看看组合是关于什么的。

A well-known principle of object-oriented programming is to prefer composition over inheritance whenever possible. Let’s see what composition is about.

回到我们PointCircle例子,我们可以创建一个Circle的孩子Point,但这不太正确。让我们扩展我们的示例并Shape清单 8.7中引入一个。我们会说我们系统中的所有形状都需要有一个标识符,因此Shape有一个idtype 属性string。ACircle是一个Shape,所以我们可以继承id。另一方面,Circle 有一个中心,所以它将包含一个center类型为 的属性Point

Going back to our Point and Circle example, we can make a Circle a child of Point, but that wouldn’t be quite right. Let’s expand our example and introduce a Shape in listing 8.7. We’ll say that all shapes in our system need to have an identifier, so Shape has an id property of type string. A Circle is a Shape, so we can inherit the id. On the other hand, the Circle has a center, so it will contain a center property of type Point.

清单 8.7。继承与组合
类形状{
    编号:字符串;

    构造函数(id:字符串){
        这个.id = id;
    }
}

类点{
    x:数字;
    y:数字;

    构造函数(x:数字,y:数字){
        这个.x = x;
        这个.y = y;
    }
}

类 Circle extends Shape {        1 
    center: Point;                 2个
    半径:数字;

    构造函数(id:字符串,中心:点,半径:数字){
        超级(身份证);
        this.center = 中心;
        this.radius = 半径;
    }
}
class Shape {
    id: string;

    constructor(id: string) {
        this.id = id;
    }
}

class Point {
    x: number;
    y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

class Circle extends Shape {       1
    center: Point;                 2
    radius: number;

    constructor(id: string, center: Point, radius: number) {
        super(id);
        this.center = center;
        this.radius = radius;
    }
}

  • 1 Circle 继承了 Shape 的 id 属性。
  • 1 Circle inherits the id property from Shape.
  • 2 Circle 包含一个 Point,它定义了其中心的 x 和 y 坐标。
  • 2 Circle contains a Point, which defines the x and y coordinates of its center.

8.3.1.有一个经验法则

8.3.1. The has-a rule of thumb

就像我们应用的is-a测试来确定我们是否应该Circle继承自一样Point,我们可以对组合应用类似的测试:has-a图 8.3)。

Just like the is-a test we applied to determine whether we should have Circle inherit from Point, we can apply a similar test for composition: has-a (figure 8.3).

图 8.3。所有形状都有一个id. 圆是一种形状,所以它继承了id. 圆有一个定义其中心的点。

我们可以定义该类型的属性,而不是从类型继承行为。这种技术仍然为我们提供包含类型存储的状态,但作为我们类型的组成部分而不是我们类型的继承部分。

Instead of inheriting behavior from a type, we can define a property of that type. This technique still gives us the state that the contained type stores but as a component part of our type rather than an inherited part of our type.

组合和 has-a 关系

组合在容器类型和包含的类型之间建立了has-a关系。如果我们的类型是Circle,而我们包含的类是Point,则关系是“Circle有一个Point”(它定义了它的中心)。这是组合的语义,也是一个很好的测试,可以应用于两种类型,以确定我们是否应该使用组合。

Composition establishes a has-a relationship between a container type and the contained type. If our type is Circle, and our contained class is Point, the relationship is “Circle has a Point” (which defines its center). This is the semantic meaning of composition and a good test to apply to two types to determine whether we should use composition.

center组合的一个主要好处是来自组件属性的所有状态(例如a 的坐标Circle)都封装在这些组件中(例如centertype 的属性Point),因此我们的类型更清晰。

A major benefit of composition is that all the state coming from component properties (such as the coordinates of the center of a Circle) is encapsulated in those components (such as the center property of type Point), so our type is much cleaner.

circle我们类型的一个实例Circle有一个circle.id属性,它继承自Shape,但其中心点的x和中心坐标在:和 中。如果我们愿意,我们可以将其设为私有,在这种情况下,外部代码将无法访问它。我们不能用继承的属性来做到这一点:如果声明为公共的,就不能隐藏它。 ycentercircle.center.xcircle.center.ycenterShapeidCircle

An instance circle of our Circle type has a circle.id property, which it inherits from Shape, but the x and y center coordinates from its center point are in center: circle.center.x and circle.center.y. If we want, we can make center private, and in that case external code wouldn’t be able to access it. We cannot do that with an inherited property: if Shape declares id as public, Circle cannot hide it.

接下来我们将讨论组合的一些应用,但一般来说,这种方法是使状态和行为对类可用的首选方法,而不是继承它。除非两种类型之间 存在明确的is-a关系,否则组合是一个很好的默认值。

We’ll go over a few applications of composition next, but in general, this method is the preferred way of making state and behavior available to a class, as opposed to inheriting it. Unless there is a clear is-a relationship between two types, composition is a good default.

8.3.2.组合类

8.3.2. Composite classes

我们将从另一个简单、直接的示例开始,因为这也是您可能熟悉的概念。它出现在面向对象编程中(以及它之外)的任何地方。

We’ll start with another simple, straightforward example because again, this is a concept you are likely familiar with. It shows up everywhere in object-oriented programming (and outside it).

一家公司有很多组成部分:各个部门、运营预算、首席执行官等等。所有这些部分都是Company. 我们在第 3 章讨论产品类型时介绍了此类类型的一个方面。如果我们暂时只看一家公司可能处于的状态集,它是每个部门所处状态、预算所处状态、首席执行官所处状态等的产物。额外的变化是我们可以通过将其设为私有来封装该状态的一部分,并使用可以在其实现中访问私有的其他方法来增强复合类(这是外部函数无法做到的)。

A company has many constituent parts: various departments, an operating budget, a CEO, and so on. All these parts are properties of Company. We covered an aspect of such types in chapter 3, when we talked about product types. If, for a moment, we simply look at the set of possible states a company can be in, it’s the product of the state each department is in, the state the budget is in, the state the CEO is in, and so on. The additional twist is that we can encapsulate parts of this state by making it private and enhance the composite class with additional methods that can access the privates in their implementation (something that an external function wouldn’t be able to do).

例如,我们不能简单地找到公司的首席执行官并问他们一个问题。我们可以尝试通过官方渠道联系公司,向 CEO 发送消息,CEO 可能会也可能不会回复我们,如下列表所示。

We can’t simply get the CEO of the company and ask them a question, for example. We can try sending a message to the CEO by contacting the company through official channels, and the CEO might or might not get back to us, as the next listing shows.

清单 8.8。询问首席执行官
班级CEO {                                            1
    isBusy(): 布尔值 {
        /* ... */
    }

    答案(问题:字符串):字符串{
        /* ... */
    }
}

类部门{
    /* ... */
}

类预算{
    /* ... */
}

公司类 {                                         2
    私人CEO:CEO = new CEO();
    私人部门:Department[] = [];
    私人预算:Budget = new Budget();

    问CEO(问题:字符串):字符串| undefined {      3 
        if (!this.ceo.isBusy()) {                       4 
            return this.ceo.answer(question);          4个
        }
    }
}
class CEO {                                           1
    isBusy(): boolean {
        /* ... */
    }

    answer(question: string): string {
        /* ... */
    }
}

class Department {
    /* ... */
}

class Budget {
    /* ... */
}

class Company {                                        2
    private ceo: CEO = new CEO();
    private departments: Department[] = [];
    private budget: Budget = new Budget();

    askCEO(question: string): string | undefined {     3
        if (!this.ceo.isBusy()) {                      4
            return this.ceo.answer(question);          4
        }
    }
}

  • 1 A CEO 很忙,可以回答问题。
  • 1 A CEO is very busy and can answer questions.
  • 2 一家公司有一位首席执行官、一组部门和一份预算。
  • 2 A company has a CEO, a set of departments, and a budget.
  • 3 如果我们想联系 CEO,我们会通过公司进行。
  • 3 If we want to contact the CEO, we do it through the company.
  • 4 如果CEO不忙,他们会接听我们的。
  • 4 If the CEO is not busy, they will answer us.

与普通的旧产品类型(如元组和记录)相比,隐藏类成员并提供对它们的受控访问的能力是封装给表带来的关键额外区别之一。

The ability to hide class members and provide controlled access to them is one of the key extra distinctions that encapsulation brings to the table compared with plain old product types such as tuples and records.

值类型和引用类型

您可能还听说过值类型引用类型,或者结构类型类型之间的区别等等。尽管那里有很多细微差别,但不幸的是,其中很少有足够的通用性。不同的编程语言以不同的方式实现这些类型,因此更多的是了解您的语言如何处理细微差别。

You might also have heard of value types and reference types, or about differences between struct and class types and so on. Although there is a lot of nuance to cover there, unfortunately, little of it is general enough. Different programming languages implement these types differently, so it’s more a matter of understanding how your language handles the nuances.

通常,当我们将值类型的实例分配给变量或将其作为参数传递给函数时,其内容会被复制到内存中,从而有效地创建一个不同的实例。另一方面,当我们分配一个引用类型的实例时,完整的状态不会被复制——只是对它的引用。旧变量和新变量都指向同一个对象并且可以改变它的状态。

In general, when we assign an instance of a value type to a variable or pass it as an argument to a function, its content gets copied in memory, effectively creating a distinct instance. On the other hand, when we assign an instance of a reference type, the full state doesn’t get copied—just a reference to it. Both the old and new variables point to the same object and can alter its state.

我们在这里没有深入讨论这个主题的原因是,由于每种语言实现这些概念的方式不同,它可能会变得非常混乱。例如,在 C# 中,结构看起来很像类,但它是值类型;分配它会导致其状态被复制。另一方面,Java 不支持开箱即用的原始数值类型之外的适当值类型:一切都是引用类型。C++ 又是不同的:C++ 中的结构简单地意味着成员在默认情况下是公共的,在类中默认是私有的。在 C++ 中,一切都是值,除非我们显式地将值声明为指针 (*) 或引用 (&)。一些函数式语言使用不可变数据,其中不存在值和引用之间的区别,

The reason why we are not covering this topic in depth here is that it might get very confusing because of the way each language implements these concepts. In C#, for example, a struct looks a lot like a class, but it is a value type; assigning it causes its state to be copied. On the other hand, Java does not support proper value types outside the primitive numerical types that come out of the box: everything is a reference type. C++, again, is different: a struct in C++ simply means that members are public by default and private by default in classes. In C++, everything is by value, unless we explicitly declare a value as pointer (*) or reference (&). Some functional languages work with immutable data, in which the distinction between value and reference doesn’t exist, as everything is moved around.

尽管值类型和引用类型之间的区别很重要(我们不想复制大量数据,因为它会影响性能;我们宁愿复制也不愿共享,因为只有一个状态所有者更安全),你应该明白您的编程语言如何表达和处理这些细微差别。

Although the difference between value and reference types matters (we don’t want to copy large amounts of data, as it affects performance; we’d rather copy than share because it’s safer to have a single owner of the state), you should understand how your programming language expresses and handles these nuances.

接下来,让我们看看另一个可能不太明显的组合应用:非常有用的适配器模式。

Next, let’s look at another, maybe not-so-obvious application of composition: the very useful adapter pattern.

8.3.3.实施适配器模式

8.3.3. Implementing the adapter pattern

适配器模式可以使两个类兼容,而不需要我们修改两个类中的任何一个。适配器的使用非常类似于物理适配器。我们可能有一台只有 USB 端口的笔记本电脑,并且想将它连接到有线网络,例如,我们会使用以太网电缆来实现。以太网到 USB 适配器管理两个不兼容组件(USB 和以太网)之间的转换,并确保它们协同工作。

The adapter pattern can make two classes compatible without requiring us to modify either of the two classes. An adapter is used very much like a physical adapter. We might have a laptop with only USB ports and want to connect it to a wired network, for example, which we would do with an Ethernet cable. An Ethernet-to-USB adapter manages the translation between the two incompatible components, USB and Ethernet, and ensures that they work together.

例如,假设我们使用一个外部几何库,它提供了一些我们需要的重要操作,但它不适合我们的对象模型。它期望根据ICircle接口定义一个圆,该接口声明两个方法来获取圆心的 x 和 y 坐标,getCenterX()以及getCenterY()另一个方法 ,getDiameter()来获取圆的直径,如以下代码所示。

As an example, let’s say we use an external geometry library that provides some important operations we need, but it doesn’t fit our object model. It expects a circle to be defined in terms of an ICircle interface that declares two methods to get the x and y coordinates of the center, getCenterX() and getCenterY(), and another method, getDiameter(), to get the diameter of the circle, as shown in the following code.

清单 8.9。几何库
命名空间几何库 {

    导出接口 ICircle {              1
        getCenterX(): 数字;
        getCenterY(): 数字;
        getDiameter():数字;
    }

    /* 省略对 ICircle 的操作 */     2

}
namespace GeometryLibrary {

    export interface ICircle {             1
        getCenterX(): number;
        getCenterY(): number;
        getDiameter(): number;
    }

    /* Operations on ICircle omitted */    2

}

  • 1 Geometry 库期望圆遵守某种契约。
  • 1 The Geometry library expects circles to adhere to a certain contract.
  • 2 我们不会详细介绍具体的操作,因为它们对我们的示例并不重要。
  • 2 We won’t go over the exact operations because they’re not important for our example.

我们Circle是根据中心定义的Point和半径定义。假设我们有一个很大的代码库,而这个圈子只是其中的一小部分,我们可能不希望 重构一切只是为了与这个库兼容。好消息是有一个更简单的解决方案:我们可以实现一个CircleAdapter类,它包装一个,实现预期的接口,并处理从 our到库预期的 Circle转换逻辑。Circle

Our Circle is defined in terms of a center Point and a radius. Assuming that we have a large codebase, and this circle is just a small piece of it, we probably don’t want to refactor everything just to be compatible with this library. The good news is that there is an easier solution: we can implement a CircleAdapter class, which wraps a Circle, implements the expected interface, and handles the logic of converting from our Circle to what the library expects.

清单 8.10。CircleAdapter
类 CircleAdapter 实现 GeometryLibrary.ICircle {     1 个
    私人圈子:Circle;                                 2个

    构造函数(圆:圆){
        this.circle = 圆圈
    }

    getCenterX(): 数字 {
        返回 this.circle.center.x;                        3个
    }

    getCenterY(): 数字 {
        返回 this.circle.center.y;                        3个
    }

    getDiameter(): 数字 {
        返回 this.circle.radius * 2;                      4个
    }
}
class CircleAdapter implements GeometryLibrary.ICircle {    1
    private circle: Circle;                                 2

    constructor(circle: Circle) {
        this.circle = circle
    }

    getCenterX(): number {
        return this.circle.center.x;                        3
    }

    getCenterY(): number {
        return this.circle.center.y;                        3
    }

    getDiameter(): number {
        return this.circle.radius * 2;                      4
    }
}

  • 1 CircleAdapter 实现库期望的 ICircle 接口。
  • 1 CircleAdapter implements the ICircle interface that the library expects.
  • 2 CircleAdapter 包装了一个 Circle 实例。
  • 2 CircleAdapter wraps a Circle instance.
  • 3 getCenterX()和getCenterY()从Circle中获取对应的x和y坐标。
  • 3 getCenterX() and getCenterY() get the corresponding x and y coordinates from Circle.
  • 4 getDiameter() 获取半径并乘以2。(直径是半径的两倍。)
  • 4 getDiameter() gets the radius and multiplies it by 2. (Diameter is twice the radius.)

现在,每当我们需要将几何库与Circle实例一起使用时,我们都会CircleAdapter为它创建一个并将其传递给几何库。适配器模式对于处理我们无法修改的代码非常有用,例如来自我们无法控制的外部库的代码。事实上,这就是适配器模式的一般结构,如图8.4所示。

Now, whenever we need to use the geometry library with a Circle instance, we create a CircleAdapter for it and pass that to the geometry library. The adapter pattern is extremely useful for dealing with code that we cannot modify, such as code that comes from external libraries outside our control. This is, in fact, the general structure of the adapter pattern as shown in figure 8.4.

图 8.4。我们有一个不兼容的IExpected接口和实际实现。通过提供实现和处理声明内容与提供内容之间的转换,Adaptee使它们兼容。 AdapterIExpectedIExpectedAdaptee

适配器可以通过将其标记为私有来隐藏它转换的实际实现。这是一个有趣的组合应用:我们不是将多个组件组合在一起,而是包装一个组件,但提供需要作为另一种类型使用的“胶水”。

The adapter can hide the actual implementation it translates from by marking it as private. This is an interesting application of composition: instead of bringing together several components, we wrap a single component but provide the “glue” it needs to be consumed as another type.

通过接口、继承和组合,我们已经涵盖了面向对象编程的最常见元素。接下来,我们将看看一个稍微更高级(也更有争议!)的概念:mix-ins。

With interfaces, inheritance, and composition out of the way, we’ve covered the most common elements of object-oriented programming. Next, we’ll look at a slightly more advanced (and more controversial!) concept: mix-ins.

8.3.4.练习

8.3.4. Exercises

1个

您将如何为FileTransfer使用 aConnection通过网络传输文件的类建模?

  1. FileTransfer扩展Connection(从Connection类型继承连接行为)。
  2. FileTransferimplements IConnection(实现一个声明连接行为的接口)。
  3. FileTransfer包装一个Connection(类成员提供连接功能)。
  4. Connectionextends abstract FileTransfer(连接扩展抽象 FileTransfer 类并提供所需的额外行为)。

1

How would you model a FileTransfer class that uses a Connection to transfer files over the network?

  1. FileTransfer extends Connection (inherits connection behavior from the Connection type).
  2. FileTransfer implements IConnection (implements an interface that declares connection behavior).
  3. FileTransfer wraps a Connection (class member provides connection functionality).
  4. Connection extends abstract FileTransfer (connection extends the abstract FileTransfer class and provides the additional behavior required).

2个

给定一个类,实现一个Airplane有两个机翼和每个机翼一个引擎的飞机。Engine尝试使用合成来对此建模。

2

Implement an Airplane with two wings and an engine on each wing, given an Engine class. Try to model this by using composition.

8.4. 扩展数据和行为

8.4. Extending data and behavior

另一种为类型引入额外数据或行为的方法并不完全是继承,但不幸的是,它主要在支持它的语言中实现。

Another way to bring in additional data or behavior to a type is not quite inheritance, though unfortunately, it is mostly implemented as such in the languages that support it.

让我们回到我们最简单的动物示例:a Catis a Petis an Animal。让我们WildAnimal在我们的层次结构中引入一个类型和Wolf它的一个子类型。野生动物可以roam(),狼也可以打猎。狩猎由三种不同的方法组成:track()stalk()pounce()图 8.5)。

Let’s go back to our simplistic animal example: a Cat is a Pet is an Animal. Let’s introduce a WildAnimal type in our hierarchy and a Wolf child type of that. Wild animals can roam(), and a wolf can also hunt. Hunting consists of three separate methods: track(), stalk(), and pounce() (figure 8.5).

图 8.5。WildAnimal用和扩展动物等级Wolf。野生动物可以roam(),而狼可以用它的track()stalk()pounce()方法捕猎。

如果需要,我们甚至可以IHunter使用标准track()stalk()pounce()方法实现一个接口。

If we want, we can even implement an IHunter interface with the standard track(), stalk(), and pounce() methods.

如果我们Tiger在组合中添加一个类型会怎样?ATiger也可以打猎,假设捕食者的狩猎行为相似,我们不想在我们的WolfTiger类型中重复代码。一种选择是在层次结构:一种类型,它是andHunter的子代WildAnimal和父代(图 8.6)。 WolfTiger

What if we add a Tiger type to the mix? A Tiger can also hunt, and assuming that hunting behavior is similar across predators, we don’t want to duplicate the code across our Wolf and Tiger types. One option is to introduce a common type in the hierarchy: a Hunter type, which is the child of WildAnimal and the parent of Wolf and Tiger (figure 8.6).

图 8.6。该类型是和Hunter的父级,并提供狩猎行为。 WolfTiger

这种方法一直有效,直到我们意识到 aCat也在狩猎。我们如何在不完全改变我们的类型层次结构的情况下使所有这些猎人行为可用Cat

This approach works until we realize that a Cat also hunts. How do we make all this hunter behavior available to Cat without completely rejiggering our type hierarchy?

8.4.1.用组合扩展行为

8.4.1. Extending behavior with composition

一种方法是定义一个IHunter接口和一个类来封装常见的狩猎行为,如清单 8.11HuntingBehavior所示。然后我们可以让所有三种类型Cat—— 、WolfTiger——包装一个Hunting-Behavior实例并将接口的实现转发给它(图 8.7)。

One way to go about it is to define an IHunter interface and a HuntingBehavior class that encapsulates the common hunting behavior, as shown in listing 8.11. Then we can have all three of our types—Cat, Wolf, and Tiger—wrap a Hunting-Behavior instance and forward the implementation of the interface to it (figure 8.7).

图 8.7。Cat, Wolf, 并Tiger包装一个实例HunterBehavior并实现IHunter接口。他们将所有调用转发给包装对象。提供所有动物实现都可以用作组件HunterBehavior的实现。不再是层次结构的一部分。 IHunterIHunterHunterBehaviorAnimal

清单 8.11。狩猎行为
接口 IHunter {                                                     1
    跟踪():无效;
    茎():无效;
    突袭():无效;
}

类 HuntingBehavior 实现 IHunter {                              2
    祈祷:动物 | 不明确的;

    跟踪():无效{
        /* ... */
    }

    茎():无效{
        /* ... */
    }

    突袭():无效{
        /* ... */
    }
}

Cat 类扩展 Pet 实现 IHunter {
    私人狩猎行为:狩猎行为=新的狩猎行为();  3个

    跟踪():无效{
        这个.huntingBehavior.track();                                  4个
    }

    茎():无效{
        这个.huntingBehavior.track();                                  4个
    }

    突袭():无效{
        这个.huntingBehavior.track();                                  4个
    }

    喵():无效{
        /* ... */
    }
}
interface IHunter {                                                    1
    track(): void;
    stalk(): void;
    pounce(): void;
}

class HuntingBehavior implements IHunter {                             2
    pray: Animal | undefined;

    track(): void {
        /* ... */
    }

    stalk(): void {
        /* ... */
    }

    pounce(): void {
        /* ... */
    }
}

class Cat extends Pet implements IHunter {
    private huntingBehavior: HuntingBehavior = new HuntingBehavior();  3

    track(): void {
        this.huntingBehavior.track();                                  4
    }

    stalk(): void {
        this.huntingBehavior.track();                                  4
    }

    pounce(): void {
        this.huntingBehavior.track();                                  4
    }

    meow(): void {
        /* ... */
    }
}

  • 1 通用IHunter接口
  • 1 Common IHunter interface
  • 2 所有狩猎动物共有的狩猎行为
  • 2 Hunting behavior common to all hunting animals
  • 3 Cat 包装了一个 HuntingBehavior 实例
  • 3 Cat wraps an instance of HuntingBehavior
  • 4 IHunter接口的所有方法都简单转发给huntingBehavior。
  • 4 All methods of IHunter interface are simply forwarded to huntingBehavior.

这种方法可行,但我们最终得到了几个IHunter通过包装实现的类HuntingBehavior。现在,将新的狩猎动物添加到我们的层次结构中会附带一堆样板,我们必须从另一种类型复制/粘贴这些样板。更糟糕的是,界面的添加IHunter会导致我们的代码库发生级联变化,因为我们必须更新每只动物的狩猎行为,即使真正改变的只是它本身HuntingBehavior

This approach works, but we end up with several classes that implement IHunter by wrapping HuntingBehavior. Adding a new hunting animal to our hierarchy now comes with a bunch of boilerplate that we have to copy/paste from another type. Even worse, an addition to the IHunter interface causes a cascade of changes in our code base, as we have to update each individual animal with hunting behavior, even though the only thing that really changes is the HuntingBehavior itself.

有没有更好的方法来实现这个?答案是肯定的,也不是。

Is there a better way of implementing this? The answer is both yes and no.

8.4.2.使用混合扩展行为

8.4.2. Extending behavior with mix-ins

让所有狩猎动物都具有这种行为的一种更简单的方法是将其混合到每种类型中。不幸的是,混合行为的方式通常是通过多重继承来实现的。这个事实是不幸的,因为它与我们在本章开头介绍的is-a经验法则不一致。我们甚至还没有涵盖多重继承的所有风险(我们也不会,但如果您好奇的话,请查看菱形继承问题)。

An easier way to have all hunting animals share this behavior is to mix it into each type. Unfortunately, the way to mix in behavior is usually achieved with multiple inheritance. This fact is unfortunate because it is at odds with what we covered at the beginning of the chapter with the is-a rule of thumb. We haven’t even covered all the perils of multiple inheritance (and we won’t, but look up the diamond inheritance problem if you are curious).

我们可以从多重继承的角度来看,创建一个Hunter实现狩猎行为的类,所有狩猎动物都派生自它。那么 aCat既是 anAnimal又是 a Hunter

We can look at this from the multiple-inheritance point of view, creating a Hunter class that implements the hunting behavior and have all hunting animals derive from it. Then a Cat is both an Animal and a Hunter.

另一方面,混合与继承不同。我们可以创建一个Hunter-Behavior实现狩猎行为的类,让所有狩猎动物都包含这种行为。

On the other hand, mix-ins aren’t the same as inheritance. We can create a Hunter-Behavior class that implements the hunting behavior and have all hunting animals include this behavior.

混入和包含关系

混入在类型与其混入类型之间建立包含关系。如果我们的班级是Cat,而我们的混入班级是HunterBehavior,则关系是“Cat包括Hunter-Behavior” 这是mix-ins的语义,不同于is-a继承关系(图8.8)。

Mix-ins establish an includes relationship between a type and its mixed-in type. If our class is Cat, and our mixed-in class is HunterBehavior, the relationship is “Cat includes Hunter-Behavior.” This is the semantic meaning of mix-ins and is different from the is-a relationship of inheritance (figure 8.8).

图 8.8。Cat, Wolf, 和Tigermix in HunterBehavior,它删除了一堆样板文件:类不再需要包装HunterBehavior对象和转发调用。他们可以简单地包括行为。

mix-in 微妙且有争议的原因是许多语言并不完全支持它们以使事情简单,并且在大多数支持它们的语言中,混合另一种类型与继承没有区别。这是有道理的,因为在我们混合了一个类之后HunterBehavior,我们的Cat类自动成为该类的子类型。Cat我们可以在任何需要的时候传入一个实例HunterBehavior,但是is-a测试失败了:a Catis not HunterBehavior

The reason why mix-ins are nuanced and controversial is that many languages don’t support them altogether to keep things simple, and in most languages that do support them, mixing in another type is indistinguishable from inheritance. This makes sense, as after we mix in a class such as HunterBehavior, our Cat class automatically becomes a subtype of that class. We can pass in a Cat instance whenever we need HunterBehavior., but the is-a test fails: a Cat is not HunterBehavior.

混合对于减少样板代码非常有用。它们允许我们通过混合不同的行为来组合一个对象,并在多种类型之间重用共同的行为。它们最适合用于实现横切关注点:影响其他关注点且不易分解的程序方面。想想引用计数、缓存、持久性等。

Mix-ins are very useful for reducing boilerplate code. They allow us to put together an object by mixing in different behaviors and to reuse common behavior across multiple types. They are best used to implement cross-cutting concerns: aspects of a program that affect other concerns and can’t be easily decomposed. Think of things like reference counting, caching, persistence, and so on.

我们将快速浏览一个 TypeScript 示例,但语法是非常特定于该语言的。如果它看起来很复杂,请不要担心;基本原则是重要的部分。

We’ll quickly go over a TypeScript example, but the syntax is very specific to the language. Don’t worry if it looks complicated; the underlying principle is the important part.

8.4.3.在 TypeScript 中混入

8.4.3. Mix-in in TypeScript

混合两种类型的一种方法是使用一个extend()函数,该函数接受两种不同类型的两个实例,并将第二个实例的所有成员复制到第一个实例中,如清单8.12所示。由于底层 JavaScript 语言的动态特性,我们可以在 TypeScript 中执行此操作。在 JavaScript 中,我们可以在运行时添加和删除对象的成员。extend()是通用的,因此它可以处理任何两种类型的实例。

One way to mix two types is to use an extend() function that takes two instances of two different types and copies all members of the second instance to the first one, as shown in listing 8.12. We can do this in TypeScript because of the dynamic nature of the underlying JavaScript language. In JavaScript, we can add and remove members of an object at run time. extend() is generic, so it can work with instances of any two types.

清单 8.12。用另一个实例的成员扩展一个实例
函数扩展<第一,第二>(第一:第一,第二:第二):
    第一和第二 {                                             1
    常量结果:未知={};

    for (const prop in first) {                                  2
        如果 (first.hasOwnProperty(prop)) {
            (<第一个>结果)[prop] = first[prop];
        }
    }
    for (const prop in second) {                                 3
        如果 (second.hasOwnProperty(prop)) {
            (<第二>结果)[prop] = second[prop];
        }
    }
    返回<第一和第二>结果;
}
function extend<First, Second>(first: First, second: Second):
    First & Second {                                            1
    const result: unknown = {};

    for (const prop in first) {                                 2
        if (first.hasOwnProperty(prop)) {
            (<First>result)[prop] = first[prop];
        }
    }
    for (const prop in second) {                                3
        if (second.hasOwnProperty(prop)) {
            (<Second>result)[prop] = second[prop];
        }
    }
    return <First & Second>result;
}

  • 1 函数返回类型是 First 和 Second 类型的组合。
  • 1 The function return type is a combination of the First and Second types.
  • 2 首先,我们遍历第一个对象的所有成员并将它们复制到结果中。
  • 2 First, we iterate over all members of the first object and copy them to the result.
  • 3 接下来,我们对第二类成员做同样的事情。
  • 3 Next, we do the same for the members of the second type.

这是我们第一次遇到&语法:First & Second定义一个类型,它具有 的所有成员First和 的所有成员Second。这在 TypeScript 中称为交集类型。不要太担心这个特定的实现;重要的是将两种类型组合成包含其两个成员的类型的概念。

This is the first time we encounter the & syntax: First & Second defines a type that has all the members of First and all the members of Second. This is called an intersection type in TypeScript. Don’t worry too much about this particular implementation; what’s important is the concept of combining two types into a type that contains both their members.

大多数语言不会让在运行时向对象添加新成员变得如此容易,但在 JavaScript 中是可能的——因此,在 TypeScript 中也是如此。作为编译时的替代方案,在 C++ 中,我们可以使用多重继承将一个类型声明为两个其他类型的组合。

Most languages don’t make it so easy to add new members to an object at run time, but it is possible in JavaScript—thus, also in TypeScript. As a compile-time alternative, in C++ we can use multiple-inheritance to declare a type as a combination of two other types.

现在我们有了我们的extend()方法,我们可以更新我们的动物示例,如清单 8.13所示。Cat我们将 a 定义MeowingPet为 的子代,而不是Pet,它是一种可以meow()但还不完全是的动物Cat,因为它没有狩猎行为。接下来,我们可以将 a 定义Cat为 的交集MeowingPet & HuntingBehavior。每当我们想要创建一个新的实例时Cat,我们都会创建一个新的实例MeowingPet,并extend()使用一个新的实例HuntingBehavior

Now that we have our extend() method, we can update our animal example as follows in listing 8.13. Instead of Cat, we define a MeowingPet as a child of Pet, which is an animal that can meow() but not quite a Cat yet, as it doesn’t have hunting behavior. Next, we can define a Cat as the intersection of MeowingPet & HuntingBehavior. Whenever we want to create a new instance of Cat, we create a new instance of MeowingPet and extend() it with a new instance of HuntingBehavior.

清单 8.13。混合行为
MeowingPet 类扩展宠物 {                                         1
    喵():无效{
        /* ... */
    }
}

类 HunterBehavior {                                                 2
    跟踪():无效{
        /* ... */
    }

    茎():无效{
        /* ... */
    }

    突袭():无效{
        /* ... */
    }
}


类型 Cat = MeowingPet & HunterBehavior;                               3个

const fluffy: Cat = extend(new MeowingPet(), new HunterBehavior());   4个
class MeowingPet extends Pet {                                        1
    meow(): void {
        /* ... */
    }
}

class HunterBehavior {                                                2
    track(): void {
        /* ... */
    }

    stalk(): void {
        /* ... */
    }

    pounce(): void {
        /* ... */
    }
}


type Cat = MeowingPet & HunterBehavior;                               3

const fluffy: Cat = extend(new MeowingPet(), new HunterBehavior());   4

  • 1 我们有一个 MeowingPet 而不是 Cat,它不完全是猫,因为它不能打猎。
  • 1 Instead of Cat, we have a MeowingPet that is not quite a Cat, as it can’t hunt.
  • 2 HunterBehavior 与我们之前的示例相同。
  • 2 HunterBehavior is the same as in our previous examples.
  • 3 Cat成为MeowingPet和HunterBehavior的交集类型。
  • 3 Cat becomes an intersection type of MeowingPet and HunterBehavior.
  • 4 我们可以通过使用 HunterBehavior 扩展 MeowingPet 来创建 Cat 的实例。
  • 4 We can create an instance of Cat by extending a MeowingPet with HunterBehavior.

我们可以将对的调用包装extend()在一个makeCat()函数中,这样可以更容易地创建Cat对象。与继承不同,通过使用混合,我们为行为的不同方面定义了不同的类型;然后我们将它们组合成一个完整的类型。我们通常有一些非常特定于一种特定类型的属性和方法——在我们的例子中,方法meow()——以及一些横切多种类型的属性和方法,例如多种动物的狩猎行为。

We can wrap the call to extend() in a makeCat() function, which makes it easier to create Cat objects. Unlike with inheritance, by using mix-ins, we define different types for different aspects of behavior; then we put them together into a complete type. We usually have some properties and methods that are very specific to one particular type—in our case, the meow() method—and some properties and methods that cross-cut across multiple types, such as the hunting behavior of multiple animals.

现在我们已经介绍了接口、继承、组合和混合——OOP 的主要元素——让我们看看纯面向对象代码的一些替代方案。

Now that we’ve covered interfaces, inheritance, composition, and mix-ins—the main elements of OOP—let’s look at a few alternatives to purely object-oriented code.

8.4.4.锻炼

8.4.4. Exercise

1个

您将如何为也可以进行跟踪(通过某种updateStatus()方法)的运输信件和包裹建模?

1

How would you model shipping letters and packages that could also have tracking (through an updateStatus() method)?

8.5. 纯面向对象代码的替代方案

8.5. Alternatives to purely object-oriented code

面向对象编程非常有用。在隐藏实现细节的同时创建具有公共接口的组件并让它们相互交互的能力是管理复杂性以及划分和征服复杂领域的关键。

Object-oriented programming is extremely useful. The ability to create components with public interfaces while hiding the implementation details and have them interact with one another is key to managing complexity and dividing and conquering complex domains.

话虽这么说,但设计软件的方法有很多,正如我们在前几章的一些例子中看到的那样,这些例子展示了不同的设计模式,例如策略、装饰器和访问者。在某些情况下,替代方案提供了更好的解耦、组件化和可重用性。

That being said, there are more ways to design software, as we’ve seen with some of the examples in earlier chapters that showed different takes on design patterns, such as strategy, decorator, and visitor. In some cases, the alternatives offer better decoupling, componentization, and reusability.

替代方案不那么流行的原因是许多语言一开始都是纯粹面向对象的,不支持函数类型和泛型之类的东西。尽管他们中的大多数都进化为支持这些东西,但许多程序员仍然 几乎完全学习早期的纯面向对象的方法。让我们快速浏览一些可用的替代方案。

The reason why the alternatives are not as popular is that many languages started as purely object-oriented, without support for things like function types and generics. Although most of them evolved to support these things, many programmers are still learning almost exclusively the purely object-oriented methods of the earlier days. Let’s quickly go over a few available alternatives.

8.5.1.求和类型

8.5.1. Sum types

我们在第 3 章介绍了求和类型,当时我们研究了一种通过使用Variant和函数来实现访问者模式的方法visit()。下面快速回顾一下使用 OOP 和不使用 OOP 时代码的外观。

We covered sum types in chapter 3, when we looked at a way to implement the visitor pattern by using a Variant and a visit() function. Following is a quick refresher on how the code looked like with OOP and without it.

这次我们选择另一个场景:一个简单的 UI 框架。PanelUI 由、Label和对象树组成Button。在一种情况下,aRenderer将在屏幕上绘制这些元素。在第二种情况下,我们XmlSerializer会将 UI 树序列化为 XML,这样我们就可以保存它并在以后重新加载它。

We’ll pick another scenario this time: a simple UI framework. The UI consists of a tree of Panel, Label, and Button objects. In one scenario, a Renderer will draw these elements on the screen. In a second scenario, an XmlSerializer will serialize the UI tree as XML that so we can save it and reload it later.

请记住,我们可以在每个 UI 元素上添加一个渲染方法和一个序列化方法,但这种技术并不理想:无论何时我们想要添加另一个场景,我们都必须接触构成 UI 的所有类。这些类最终也对使用它们的环境了解得太多了。相反,我们可以使用一种访问者模式,它将场景与 UI 小部件分离,并让它们不关心它们将如何在我们的应用程序中使用,如以下清单所示。

Remember that we could add a method to render and a method to serialize on each of the UI elements, but that technique is not ideal: whenever we want to add another scenario, we have to touch all the classes that make up the UI. These classes also end up knowing way too much about the environment in which they are used. Instead, we can use a visitor pattern that will decouple the scenarios from the UI widgets and keep them oblivious to how they will be used in our application, as shown in the following listing.

清单 8.14。OOP 访问者
界面 IVisitor {
    访问面板(面板:面板):无效;
    访问标签(标签:标签):无效;
    访问按钮(按钮:按钮):无效;
}

类渲染器实现 IVisitor {
    访问面板(面板:面板){ /* ... */ }
    visitLabel(label: 标签) { /* ... */ }
    visitButton(按钮:按钮) { /* ... */ }
}

类 XmlSerializer 实现 IVisitor {
    访问面板(面板:面板){ /* ... */ }
    visitLabel(label: 标签) { /* ... */ }
    visitButton(按钮:按钮) { /* ... */ }
}

接口 IUIWidget {
    接受(访客:IVisitor):无效;
}

类面板实现 IUIWidget {
    /* 面板成员省略 */
    接受(访客:IVisitor){
        visitor.visitPanel(这个);
    }
}

类标签实现 IUIWidget {
    /* 标签成员省略 */
    接受(访客:IVisitor){
        visitor.visitLabel(这个);
    }
}

类按钮实现 IUIWidget {
    /* 按钮成员省略 */
    接受(访客:IVisitor){
        visitor.visitButton(这个);
    }
}
interface IVisitor {
    visitPanel(panel: Panel): void;
    visitLabel(label: Label): void;
    visitButton(button: Button): void;
}

class Renderer implements IVisitor {
    visitPanel(panel: Panel) { /* ... */ }
    visitLabel(label: Label) { /* ... */ }
    visitButton(button: Button) { /* ... */ }
}

class XmlSerializer implements IVisitor {
    visitPanel(panel: Panel) { /* ... */ }
    visitLabel(label: Label) { /* ... */ }
    visitButton(button: Button) { /* ... */ }
}

interface IUIWidget {
    accept(visitor: IVisitor): void;
}

class Panel implements IUIWidget {
    /* Panel members omitted */
    accept(visitor: IVisitor) {
        visitor.visitPanel(this);
    }
}

class Label implements IUIWidget {
    /* Label members omitted */
    accept(visitor: IVisitor) {
        visitor.visitLabel(this);
    }
}

class Button implements IUIWidget {
    /* Button members omitted */
    accept(visitor: IVisitor) {
        visitor.visitButton(this);
    }
}

在 OOP 实现中,我们需要IVisitorIUIWidget接口将系统粘合在一起。所有 UI 小部件都需要知道如何IVisitor使事情正常进行,即使这不是必需的。

In the OOP implementation, we need IVisitor and IUIWidget interfaces to glue the system together. All UI widgets need to know about IVisitor to make things work, even though that shouldn’t be necessary.

另一种实现方式——使用 a——Variant消除了对接口的需求,并且文档项不需要知道访问者的存在。

The alternative implementation—using a Variant—removes the need for interfaces, and document items don’t need to know that visitors exist.

清单 8.15。访客与Variant
类渲染器{
    renderPanel(panel: Panel) { /* ... */ }
    renderLabel(label: 标签) { /* ... */ }
    渲染按钮(按钮:按钮){ /* ... */ }
}

类 XmlSerializer {
    serializePanel(panel: Panel) { /* ... */ }
    serializeLabel(label: 标签) { /* ... */ }
    序列化按钮(按钮:按钮){ /* ... */ }
}

类面板{
    /* 面板成员省略 */
}

类标签{
    /* 标签成员省略 */
}

类按钮{
    /* 按钮成员省略 */
}

让小部件:变体<面板,标签,按钮> =
    Variant.make1(新面板());                          1个

让序列化器:XmlSerializer = new XmlSerializer();

访问(小部件,                                             2
    (面板:面板)=> serializer.serializePanel(面板),
    (标签:标签)=> serializer.serializeLabel(标签),
    (按钮:按钮)=> serializer.serializeButton(按钮)
);
class Renderer {
    renderPanel(panel: Panel) { /* ... */ }
    renderLabel(label: Label) { /* ... */ }
    renderButton(button: Button) { /* ... */ }
}

class XmlSerializer {
    serializePanel(panel: Panel) { /* ... */ }
    serializeLabel(label: Label) { /* ... */ }
    serializeButton(button: Button) { /* ... */ }
}

class Panel {
    /* Panel members omitted */
}

class Label {
    /* Label members omitted */
}

class Button {
    /* Button members omitted */
}

let widget: Variant<Panel, Label, Button> =
    Variant.make1(new Panel());                          1

let serializer: XmlSerializer = new XmlSerializer();

visit(widget,                                            2
    (panel: Panel) => serializer.serializePanel(panel),
    (label: Label) => serializer.serializeLabel(label),
    (button: Button) => serializer.serializeButton(button)
);

  • 1我们在 第3章定义的Variant类型可以存储不相关的类型。
  • 1 The Variant type we defined in chapter 3 can store types that are not related.
  • 2 visit() 将系统粘合在一起,将 UI 小部件与序列化程序方法相匹配。
  • 2 visit() glues the system together, matching the UI widget with the serializer method.

请注意,我们显示的是Variantvisit()正在使用,但从技术上讲,OOP 示例的等价物只是前五个类定义。请注意,不需要任何接口。

Note that we are showing the Variant and visit() being used, but technically, the equivalent of the OOP example is just the first five class definitions. Notice that no interfaces are needed.

一般来说,如果我们想以相同的方式传递不同类型的对象或将它们放在一个公共集合中,则它们不一定需要实现相同的接口或具有公共的父级。相反,我们可以使用 sum 类型,它可以在不强制类型之间存在任何关系的情况下实现相同的行为。

In general, if we want to pass around objects of different types in the same manner or put them in a common collection, they don’t necessarily need to implement the same interface or have a common parent. Instead, we can use a sum type, which enables the same behavior without enforcing any relationship between the types.

8.5.2.函数式编程

8.5.2. Functional programming

在 OOP 语言支持函数类型之前,我们必须将任何行为片段包装在一个类中。正如我们在第 5 章中看到的,一个典型的策略模式实现需要一个行为接口和几个类来实现该接口。

Before OOP languages supported function types, we had to wrap any piece of behavior in a class. As we saw in chapter 5, a typical strategy pattern implementation required an interface for the behavior and several classes to implement the interface.

让我们回顾一下第 5 章中的图,其中描述了策略模式的两种替代实现(图 8.9)。

Let’s review the figures from chapter 5, which described the two alternative implementations for the strategy pattern (figure 8.9).

图 8.9。面向对象的策略模式。算法的不同版本在ConcreteStrategy1和中实现ConcreteStrategy2

如果我们可以将算法实现作为函数传递,这可以简化很多。我们使用函数类型代替接口;我们使用函数代替类(图 8.10)。

This can be simplified a lot if we can just pass the algorithm implementation as a function. Instead of an interface, we use a function type; instead of classes, we use functions (figure 8.10).

图 8.10。功能策略模式。算法的不同版本被实现为函数。

函数式编程也避免维护状态:一个函数可以接受一组参数,执行一些计算,并在不改变任何状态的情况下返回结果。

Functional programming also avoids maintaining state: a function can take a set of arguments, perform some computation, and return the result without changing any state.

让我们重新审视清单 8.16中的二进制表达式示例,看看函数式实现的样子。如果我们将表达式定义为计算结果为数字的东西,我们可以将 our 替换为不带参数并返回数字的IExpression函数类型。Expression代替 a SumExpression,我们可以实现一个工厂函数makeSumExpression(),给定两个数字,返回一个可以将它们相加的闭包。请记住闭包捕获状态——在本例中为 ab参数。乘法也是如此。

Let’s revisit our binary expression example in listing 8.16 and see how a functional implementation would look. If we define an expression as something that evaluates to a number, we can replace our IExpression with a function type Expression that takes no arguments and returns a number. Instead of a SumExpression, we can implement a factory function makeSumExpression() that, given two numbers, returns a closure that can add them up. Remember that a closure captures state—in this case, the a and b arguments. The same is true for multiplication.

清单 8.16。函数表达式
输入表达式 = () => 数字;                                 1个

function makeSumExpression(a: number, b: number): 表达式 {
    返回 () => a + b;                                         2个
}

函数 makeMulExpression(a: 数字, b: 数字): 表达式 {
    返回 () => a * b;                                         3 
}
type Expression = () => number;                                 1

function makeSumExpression(a: number, b: number): Expression {
    return () => a + b;                                         2
}

function makeMulExpression(a: number, b: number): Expression {
    return () => a * b;                                         3
}

  • 1 Expression 函数类型取代了 IExpression。
  • 1 The Expression function type replaces IExpression.
  • 2 makeSumExpression() 返回闭包() => a + b。
  • 2 makeSumExpression() returns the closure () => a + b.
  • 3 makeMulExpression() 返回闭包() => a * b。
  • 3 makeMulExpression() returns the closure () => a * b.

我们不再需要BinaryExpression;该类过去用于保存状态,但现在状态被包裹在闭包中。

We no longer need BinaryExpression; that class used to hold state, but now state is wrapped in the closures.

如果我们IExpression更复杂,声明多个方法,那么面向对象的方法可能会更好。但是请留意一些简单的情况,在这些情况下,您可以使用函数式方法以更少的代码实现相同的行为。

If our IExpression were more complex, declaring multiple methods, the object-oriented approach might have worked better. But keep an eye out for simple cases in which you can achieve the same behavior with much less code by using a functional approach.

8.5.3.泛型编程

8.5.3. Generic programming

纯面向对象编程的另一种选择是泛型编程。到目前为止,我们已经在许多代码示例中使用了泛型,但还没有深入介绍它们。我们将在接下来的两章中这样做,我们将看到抽象和重用代码的不同方法。

The other alternative to purely object-oriented programming is generic programming. We’ve used generics in many code examples thus far but haven’t covered them in depth yet. We’ll do that in the next two chapters, and we’ll see different ways to abstract and reuse code.

本节的要点不应该是避免面向对象编程;它是我们可以用来解决范围广泛的问题的重要工具。得出的结论是,我们应该牢记一些替代方案。我们应该选择使我们的代码尽可能安全、清晰和松散耦合的方法。

The takeaway from this section shouldn’t be to avoid object-oriented programming; it is an important tool that we can use to solve a broad range of problems. The takeaway is that there are alternatives that we should keep in mind. We should pick the approach that makes our code as safe, as clear, and as loosely coupled as possible.

概括

Summary

  • 我们使用接口来指定合约。接口可以扩展和组合。
  • We use interfaces to specify contracts. Interfaces can be extended and combined.
  • is -a经验法则可以很好地检验我们何时应该使用继承。
  • The is-a rule of thumb is a good test for when we should use inheritance.
  • 我们使用继承来表示层次结构或通过使用抽象或重写方法来实现参数化行为。
  • We use inheritance to represent hierarchies or to implement parameterized behavior by using abstract or overridden methods.
  • has-a经验法则是我们何时应该使用组合的一个很好的测试。
  • The has-a rule of thumb is a good test for when we should use composition.
  • 我们使用组合将多个部分封装到一个类型中。
  • We use composition to encapsulate multiple parts into a single type.
  • 适配器模式是一个示例,其中我们利用封装和组合来使类型适应不同的接口而无需修改它。
  • The adapter pattern is an example in which we leverage encapsulation and composition to adapt a type to a different interface without modifying it.
  • 我们使用 mix-ins 将行为添加到类型中。
  • We use mix-ins to add behavior into a type.
  • Sum 类型、函数式编程和泛型编程是我们应该牢记的纯 OOP 的替代方案。它们不会取代 OOP;相反,它们在某些情况下更好。
  • Sum types, functional programming, and generic programming are alternatives to pure OOP that we should keep in mind. They don’t replace OOP; rather, they are better in some cases.

在本章中我们只简单地谈到了泛型,因为接下来的两章将专门讨论这个主题。继续阅读!

We touched only briefly on generics in this chapter, as the next two chapters will focus exclusively on that topic. Read on!

习题答案

Answers to exercises

使用接口定义合约

Defining contracts with interfaces

1个

c—从功能的角度来看index(),这显然是一个契约,所以期待一个INamed接口是方法。

1

c—From the point of view of the index() function, this is clearly a contract, so expecting an INamed interface is the approach.

2个

我们可以简单地通过组合其他两个接口来定义这个接口:

接口 IterableIterator<T> 扩展 Iterable<T>, Iterator<T> {
}

2

We can define this interface simply by combining the two other interfaces:

interface IterableIterator<T> extends Iterable<T>, Iterator<T> {
}

 

 

继承数据和行为

Inheriting data and behavior

1个

d——即使只看类名,我们也可以看出这三个例子都没有描述 is -a关系,所以它们看起来都不像继承的好用法。

1

d—Even by just seeing the class name, we can tell that none of the three examples describe an is-a relationship, so none of them look like a good use of inheritance.

2个

使用继承的可能实现:

抽象类 UnaryExpression 实现 IExpression {
    只读一个:数字;
    构造函数(a:数字){
        这个.a = a;
    }

    抽象评估():数字;
}

类 UnaryMinusExpression 扩展 UnaryExpression {
    评估():数字{
        返回 -this.a;
    }
}

2

A possible implementation using inheritance:

abstract class UnaryExpression implements IExpression {
    readonly a: number;
    constructor(a: number) {
        this.a = a;
    }

    abstract eval(): number;
}

class UnaryMinusExpression extends UnaryExpression {
    eval(): number {
        return -this.a;
    }
}

 

 

组合数据和行为

Composing data and behavior

1个

c—这个场景很适合使用组合。Connection应该是 的成员FileTransfer,因为 需要它FileTransfer,但两种类型都不应直接扩展另一种。

1

c—This scenario is a good one for using composition. Connection should be a member of FileTransfer, as it is needed by FileTransfer, but neither type should directly extend the other.

2个

使用组合的可能实现:

类翼{
    只读引擎:Engine = new Engine();
}

类飞机{
    只读 leftWing: Wing = new Wing();
    只读 rightWing: Wing = new Wing();
}

2

A possible implementation using composition:

class Wing {
    readonly engine: Engine = new Engine();
}

class Airplane {
    readonly leftWing: Wing = new Wing();
    readonly rightWing: Wing = new Wing();
}

 

 

扩展数据和行为

Extending data and behavior

1个

对此建模的一种方法是在类中提供跟踪行为,然后将其与类Tracking混合以向它们添加跟踪行为。在 TypeScript 中,这可以通过如下方法完成: LetterPackageextend()

类字母 { /*...*/ }
类包 { /*...*/ }

类跟踪{
    setStatus(status: Status) { /*...*/ }
}

输入 LetterWithTracking = Letter & Tracking;
输入 PackageWithTracking = 包裹和跟踪;

1

One way to model this is to provide tracking behavior in a Tracking class and then mix it in with Letter and Package classes to add tracking behavior to them. In TypeScript, this can be done with a method like extend():

class Letter { /*...*/ }
class Package { /*...*/ }

class Tracking {
    setStatus(status: Status) { /*...*/ }
}

type LetterWithTracking = Letter & Tracking;
type PackageWithTracking = Package & Tracking;

 

 

第 9 章。通用数据结构

Chapter 9. Generic data structures

本章涵盖

This chapter covers

  • 分离独立关注点
  • Separating independent concerns
  • 使用通用数据结构进行数据布局
  • Using generic data structures for data layout
  • 遍历任何数据结构
  • Traversing any data structure
  • 设置数据处理管道
  • Setting up a data processing pipeline

我们将从涵盖应该使用它们的常见情况开始我们对泛型类型的讨论:制作独立的、可重用的组件。我们将查看几个可以从身份函数(仅返回其参数的函数)中受益的场景,并查看此类函数的通用实现。我们还将回顾Optional<T>我们在第 3 章中构建的类型,作为另一种简单但功能强大的泛型类型。

We’ll start our discussion of generic types by covering a common case in which they should be used: making independent, reusable components. We’ll look at a couple of scenarios in which we would benefit from an identity function (a function that simply returns its argument) and see a generic implementation of such a function. We’ll also review the Optional<T> type we built in chapter 3 as another simple but powerful generic type.

接下来,我们将讨论数据结构。数据结构可以塑造我们的数据,而不必知道数据是什么。使这些结构通用化允许我们为各种值重用形状,显着减少我们需要编写的代码量。我们将从数字的二叉树和字符串的链表开始,并从中派生出通用的二叉树和链表。

Next, we’ll talk about data structures. Data structures give shape to our data without having to be aware of what the data is. Making these structures generic allows us to reuse the shape for all sorts of values, significantly reducing the amount of code we need to write. We’ll start with a binary tree of numbers and a linked list of strings, and derive a generic binary tree and linked list from them.

通用数据结构并不能解决我们所有的问题:我们仍然需要遍历它们。我们将看到如何使用迭代器为遍历任何数据结构提供通用接口。这也有助于我们减少所需的代码量,因为我们不必为每个数据结构提供不同版本的函数,而是提供与迭代器一起使用的单一版本。同样,我们将使用我们在第 6 章中介绍的生成器。这些可恢复函数产生值,我们可以使用它们在我们的数据结构上实现迭代器。

Generic data structures don’t solve all our problems: we still need to traverse them. We’ll see how we can use iterators to provide a common interface for traversing any data structure. This also helps us reduce the amount of code we need, as we don’t have to provide different versions of functions for each data structure, but a single version that works with iterators. Again, we’ll use generators, which we introduced in chapter 6. These resumable functions yield values, and we can use them to implement iterators over our data structures.

最后,我们将讨论将函数链接到处理管道中并在可能无限的数据流上运行它们。

Finally, we’ll talk about chaining functions into processing pipelines and running them over potentially infinite streams of data.

9.1. 解耦问题

9.1. Decoupling concerns

让我们用一个简单的例子来介绍泛型:我们有一个函数 ,getNumbers()它给我们一个数字数组,但允许我们在返回它们之前对它们应用转换。这是通过transform()接受一个数字并返回一个数字的参数来完成的。调用者可以传入这样一个transform()函数,并getNumbers()在返回结果之前应用它,如下一个清单所示。

Let’s introduce generics with a simple example: we have a function, getNumbers(), that gives us an array of numbers but allows us to apply a transformation to them before returning them. This is done with a transform() argument that takes a number and returns a number. Callers can pass in such a transform() function, and getNumbers() will apply it before returning its result, as shown in the next listing.

清单 9.1。getNumbers()
输入 TransformFunction = (value: number) => number;     1个

函数 getNumbers(
    变换:变换函数):数字[] {            2
    /* ... */
}
type TransformFunction = (value: number) => number;     1

function getNumbers(
    transform: TransformFunction): number[] {           2
    /* ... */
}

  • 1 接受一个数字并返回一个数字的函数类型
  • 1 The type of a function that takes a number and returns a number
  • 2 调用者提供一个 transform(),它在返回结果数组之前应用于每个数字。
  • 2 Callers provide a transform() that gets applied to each number before being returned in the result array.

如果调用方不需要应用任何转换怎么办?一个好的默认值transform()是一个不做任何事情的函数——一个只返回其结果的函数,如以下清单所示。

What if the callers don’t need to apply any transformation? A good default for this transform() would be a function that doesn’t do anything—one that simply returns its result, as shown in the following listing.

清单 9.2。默认transform()
输入 TransformFunction = (value: number) => number;

函数 doNothing(值:数字):数字 {                   1
    返回值;
}

函数 getNumbers(
    变换:TransformFunction = doNothing ): number[] {     2
    /* ... */
}
type TransformFunction = (value: number) => number;

function doNothing(value: number): number {                  1
    return value;
}

function getNumbers(
    transform: TransformFunction = doNothing): number[] {    2
    /* ... */
}

  • 1 doNothing() 只是返回其参数而不应用任何转换。
  • 1 doNothing() simply returns its argument without applying any transformation.
  • 2 getNumbers() 默认使用 doNothing(),因此如果调用者不需要应用任何转换,则可以跳过提供参数。
  • 2 getNumbers() uses doNothing() as a default, so callers can skip providing an argument if they don’t need any transformation applied.

让我们看另一个例子。假设我们有一个对象数组和一种从对象Widget创建对象的方法。函数处理对象数组并返回对象数组。因为我们不想组装超过需要的东西,所以将一个函数作为参数,给定一个对象数组,该函数返回该数组的一个子集,如以下代码所示。这允许调用者告诉函数哪些小部件真正需要组装,因此其余的可以忽略。 AssembledWidgetWidgetassembleWidgets()WidgetAssembledWidgetassembleWidgets()pluck()Widget

Let’s look at another example. Assume that we have an array of Widget objects and a way to create an AssembledWidget object out of a Widget object. An assembleWidgets() function handles an array of Widget objects and returns an array of AssembledWidget objects. Because we don’t want to assemble more than needed, assembleWidgets() takes as argument a pluck() function, which, given an array of Widget objects, returns a subset of this array, as shown in the following code. This allows callers to tell the function which widgets really need assembling, so the rest can be ignored.

清单 9.3。assembleWidgets()
输入 PluckFunction = (widgets: Widget[]) => Widget[];     1个

函数 assembleWidgets(
    采摘: PluckFunction): AssembledWidget[] {             2
    /* ... */
}
type PluckFunction = (widgets: Widget[]) => Widget[];     1

function assembleWidgets(
    pluck: PluckFunction): AssembledWidget[] {            2
    /* ... */
}

  • 1 函数的类型,它接受一个小部件数组并返回该数组的一个子集
  • 1 The type of a function that takes an array of widgets and returns a subset of that array
  • 2 调用者提供 pluck(),assembleWidgets() 调用它来选择需要组装的小部件。
  • 2 Callers provide pluck(), which assembleWidgets() calls to select the widgets that need assembly.

这个函数的默认值是什么pluck()?我们可以说,如果调用者不提供函数pluck(),我们将转换整个小部件列表。让我们调用这个默认值pluckAll()并让它在下一个清单中简单地返回它的参数。

What would be a good default for this pluck() function? We can say that if the caller doesn’t supply a pluck() function, we transform the whole list of widgets. Let’s call this default pluckAll() and have it simply return its argument in the next listing.

清单 9.4。默认pluck()
输入 PluckFunction = (widgets: Widget[]) => Widget[];

function pluckAll(widgets: Widget[]): Widget[] {              1
    返回小部件;
}

函数 assembleWidgets(
    采摘: PluckFunction = pluckAll ): AssembledWidget[] {     2
    /* ... */
}
type PluckFunction = (widgets: Widget[]) => Widget[];

function pluckAll(widgets: Widget[]): Widget[] {             1
    return widgets;
}

function assembleWidgets(
    pluck: PluckFunction = pluckAll): AssembledWidget[] {    2
    /* ... */
}

  • 1 pluckAll() 简单地返回它得到的整个数组。
  • 1 pluckAll() simply returns the whole array it gets.
  • 2 如果用户自己不提供 pluck(),我们使用 pluckAll() 作为参数的默认值。
  • 2 We use pluckAll() as a default value for the argument if the user doesn’t provide a pluck() themselves.

并排查看我们的两个示例,我们可以看到doNothing()pluckAll()非常相似:它们都接受一个参数并在不进行任何处理的情况下返回它,如以下清单所示。

Looking at our two examples side by side, we can see that doNothing() and pluckAll() are very similar: they both take an argument and return it without doing any processing, as the following listing shows.

清单 9.5。doNothing()pluckAll()
函数 doNothing(值:数字):数字 {
    返回值;
}

function pluckAll(widgets: Widget[]): Widget[] {
    返回小部件;
}
function doNothing(value: number): number {
    return value;
}

function pluckAll(widgets: Widget[]): Widget[] {
    return widgets;
}

它们之间的区别在于它们获取和返回值的类型:do-Nothing()使用数字,并pluckAll()使用对象数组Widget。这两个函数都是恒等函数。在代数中,恒等函数是一个函数f(x) = x

The difference between them is the type of the value they take and return: do-Nothing() uses a number, and pluckAll() uses an array of Widget objects. Both functions are identity functions. In algebra, an identity function is a function f(x) = x.

9.1.1.可重用的身份函数

9.1.1. A reusable identity function

我们必须创建两个非常相似的独立函数,这不是很好。这种方法不能很好地扩展。我们可以通过编写一个可重用的身份函数来简化这个过程吗?答案是肯定的。

It’s not great that we had to create two separate functions that are so similar. This approach doesn’t scale well. Can we simplify this process by writing a reusable identity function? The answer is yes.

让我们从一种简单的方法开始,因为身份对于任何类型都是相同的,所以我们简单地使用any. 这将为我们提供一个identity()函数,该函数接受 type 的值any并返回 type 的值any,如下一个清单所示。

Let’s start with a naïve approach and say that because identity is the same for any type, we simply use any. This would give us an identity() function that takes a value of type any and returns a value of type any, as shown in the next listing.

清单 9.6。天真的身份
函数标识(值:任何):任何{
    返回值;
}
function identity(value: any): any {
    return value;
}

此实现的问题在于,当我们开始使用 时any,我们绕过了类型检查器并失去了类型安全性,如以下清单所示。identity()我们可以将使用字符串调用的 结果传递给需要数字的函数,代码编译得很好,但在运行时会失败。

The problem with this implementation is that when we start using any, we bypass the type checker and lose type safety, as shown in the following listing. We can pass the result of calling identity() with a string to a function that expects a number, and the code will compile just fine, but it will fail at run time.

清单 9.7。不安全使用any
函数平方(x:数字):数字{
    返回 x * x;
}

广场(身份(“你好!”));       1个
function square(x: number): number {
    return x * x;
}

square(identity("Hello!"));       1

  • 1 这编译并在运行时失败,因为 any 绕过了正常的类型检查。
  • 1 This compiles and fails at run time because any bypasses the normal type checks.

有一种更安全的方法可以做到这一点:参数化函数之间的不同之处,即参数的类型。该参数将是一个类型参数。

There is a safer way to do this: parameterize what is different between the functions, namely the type of their argument. This parameter will be a type parameter.

类型参数

类型参数是泛型类型名称的标识符。类型参数用作客户端在创建泛型实例时指定的特定类型的占位符。

A type parameter is an identifier for a generic type name. Type parameters are used as placeholders for specific types that the client specifies when creating an instance of the generic type.

在下一个清单中,我们的通用身份将使用类型参数T,这将number在第一种情况和Widget[]第二种情况下出现。

In the next listing, our generic identity will use a type parameter T, which will be number in the first case and Widget[] in the second case.

清单 9.8。通用标识
函数标识<T>(值: T): T {                           1
    返回值;
}

函数 getNumbers(
    转换:TransformFunction = identity):number[] {      2
    /* ... */
}
函数 assembleWidgets(
    采摘: PluckFunction = identity ): AssembledWidget[] {     3
    /* ... */
}
function identity<T>(value: T): T {                          1
    return value;
}

function getNumbers(
    transform: TransformFunction = identity): number[] {     2
    /* ... */
}
function assembleWidgets(
    pluck: PluckFunction = identity): AssembledWidget[] {    3
    /* ... */
}

  • 1 带有类型参数 T 的通用身份函数
  • 1 Generic identity function with a type parameter T
  • 2 我们可以使用 identity() 而不是 doNothing()。T 在这种情况下变成数字。
  • 2 We can use identity() instead of doNothing(). T becomes number in this case.
  • 3 我们可以使用 identity() 而不是 pluckAll()。在这种情况下,T 变为 Widget[]。
  • 3 We can use identity() instead of pluckAll(). T becomes Widget[] in this case.

T编译器足够聪明,无需我们拼写就可以弄清楚应该是什么。我们不再需要doNothing()and pluckAll(),如果我们需要恒等函数,我们可以将它与任何其他类型重用。现在,当确定类型时,例如当getNumbers()caseT为 时number,编译器可以执行类型检查,并且我们不再像尝试字符串那样结束square(),如下一个清单所示。

The compiler is smart enough to figure out what T should be without our having to spell it out. We no longer need doNothing() and pluckAll(), and we can reuse this with any other type if we need an identity function. Now when the type is determined, such as when the getNumbers() case T is number, the compiler can perform type checking, and we no longer end up in a situation like attempting to square() a string, as shown in the next listing.

清单 9.9。类型安全
函数标识<T>(值:T):T {
    返回值;
}

广场(身份(“你好!”));        1个
function identity<T>(value: T): T {
    return value;
}

square(identity("Hello!"));        1

  • 1 这不再编译。
  • 1 This no longer compiles.

我们可以想出这个实现,因为无论使用何种类型的函数,恒等函数的机制都是相同的。getNumbers()我们有效地将恒等逻辑与和的问题域分离,assembleWidgets()因为恒等逻辑和问题域是正交的,或者说是独立的。

We could come up with this implementation because the mechanics of the identity function are the same regardless of the type the function is used with. We effectively decoupled the identity logic from the problem domain of getNumbers() and assembleWidgets() because the identity logic and the problem domain are orthogonal, or independent.

9.1.2.可选类型

9.1.2. The optional type

作为另一个例子,看看Optional我们在第 3 章中提供的实现。请记住,可选类型包含某种类型的值T或不包含任何内容。

As another example, take a look at the Optional implementation we provided in chapter 3. Remember that an optional type contains a value of some type T or doesn’t contain anything.

清单 9.10。Optional类型
可选类 <T> {                           1
    私有值:T | 不明确的;
    私人分配:布尔值;

    构造函数(值?:T){                  2
        如果(值){
            this.value = 值;
            this.assigned = true;
        } 别的 {
            this.value = undefined;
            this.assigned = false;
        }
    }

    有值():布尔值{
        返回this.assigned;
    }

    getValue(): T {
        如果(!this.assigned)抛出错误();   3个

        返回<T>这个值;
    }
}
class Optional<T> {                          1
    private value: T | undefined;
    private assigned: boolean;

    constructor(value?: T) {                 2
        if (value) {
            this.value = value;
            this.assigned = true;
        } else {
            this.value = undefined;
            this.assigned = false;
        }
    }

    hasValue(): boolean {
        return this.assigned;
    }

    getValue(): T {
        if (!this.assigned) throw Error();   3

        return <T>this.value;
    }
}

  • 1 Optional 包装了一个通用类型 T。
  • 1 Optional wraps a generic type T.
  • 2 值是可选参数,因为 TypeScript 不支持构造函数重载。
  • 2 value is an optional argument because TypeScript doesn’t support constructor overloads.
  • 3 如果未分配值,则尝试获取值会引发异常。
  • 3 If a value is not assigned, attempting to get a value throws an exception.

同样,处理值缺失的逻辑独立于值的实际类型。我们有一个Optional可以存储任何其他类型的通用类型,因为它将以相同的方式处理任何事情。你可以认为Optional是在一个完全不同的维度中T,因为我们所做的任何改变都Optional不会影响T,而任何改变都T不会影响Optional。这种隔离是泛型编程的一个极其强大的特性。

The logic of handling the absence of a value is, again, independent of the actual type of the value. We have a generic Optional type that can store any other type, as it will handle anything in the same way. You can think of Optional as being in a completely different dimension from T, as any changes we make to Optional do not affect T, and any changes made to T do not affect Optional. This isolation is an extremely powerful feature of generic programming.

9.1.3.通用类型

9.1.3. Generic types

我们刚刚看到了泛型的两种用法:泛型函数和泛型类。现在让我们退后一步,看看是什么让泛型类型如此特殊。我们通过查看基本类型和组合它们的方法开始本书。我们有诸如booleanand之类的类型number,以及诸如boolean | number. 我们有函数类型,例如() => number. 正如我们所见,这些类型都没有任何类型参数。一个数字就是一个数字。返回数字的函数是返回数字的函数。

We just saw two uses of generics: a generic function and a generic class. Now let’s step back and look at what makes generic types special. We started the book by looking at basic types and ways to combine them. We have types such as boolean and number, and types such as boolean | number. We have function types such as () => number. As we can see, none of these types has any type parameter. A number is a number. A function that returns a number is a function that returns a number.

当我们引入泛型时,这种情况发生了变化。我们有一个(value: T) => T带有类型参数的通用函数T。当我们为 指定实际类型时,我们创建了特定的函数TWidget[]例如,如果我们使用,我们最终会得到一个函数类型(value: Widget[]) => Widget[]。这是我们第一次可以插入类型并获得不同的类型定义(图 9.1)。

When we introduce generics, this situation changes. We have a generic function (value: T) => T, with a type parameter T. We create specific functions when we specify an actual type for T. If we use Widget[], for example, we end up with a function type (value: Widget[]) => Widget[]. This is the first time when we can plug in types and get different type definitions (figure 9.1).

图 9.1。具有类型参数T和两个实例的通用标识:identity<number>()具有具体类型(value: number) => numberidentity<Widget[]>()具有具体类型(value: Widget[]) => Widget[]

通用类型

泛型类型是在一个或多个类型上参数化的泛型函数、类、接口等。通用类型允许我们编写适用于不同类型的通用代码,从而实现高水平的代码重用。

A generic type is a generic function, class, interface, and so on that is parameterized over one or more types. Generic types allow us to write general code that works with different types, enabling a high level of code reuse.

正如我们在前面的示例中看到的,以及我们将在本章和下一章中看到的那样,能够使用泛型可以使我们的代码更好地组件化。我们可以将这些通用组件用作构建块,并将它们组合起来以实现所需的行为,同时将它们之间的依赖性降到最低。超越我们的简单identity<T>()Optional<T>示例,让我们看看数据结构。

As we saw in the previous examples, and as we’ll see throughout this chapter and the next, being able to use generics makes our code much better componentized. We can use these generic components as building blocks and combine them to achieve the desired behavior while having minimal dependency between them. Moving beyond our simple identity<T>() and Optional<T> examples, let’s look at data structures.

9.1.4.练习

9.1.4. Exercises

1个

实现一个Box<T>简单地包装 type 值的泛型T

1

Implement a generic Box<T> type that simply wraps a value of type T.

2个

实现一个unbox<T>()接受 aBox<T>并返回装箱值的通用函数。

2

Implement a generic unbox<T>() function that takes a Box<T> and returns the boxed value.

9.2. 通用数据布局

9.2. Generic data layout

让我们从几个非泛型的例子开始:一棵数字的二叉树,如清单 9.11所示,以及一个字符串链表,如清单 9.12所示。我相信您熟悉这些简单的数据结构。我们将树实现为一个或多个节点,每个节点存储一个数值和对其左右子节点的引用。这些引用可以指向节点,或者undefined在没有子节点的情况下。

Let’s start with a couple of nongeneric examples: a binary tree of numbers, shown in listing 9.11, and a linked list of strings shown in listing 9.12. I’m sure that you are familiar with these simple data structures. We will implement the tree as one or more nodes, each node storing a number value and references to its left and right children. These refences can be to nodes or undefined in case there is no child node.

清单 9.11。数字的二叉树
类 NumberBinaryTreeNode {
    值:数字;
    左:NumberBinaryTreeNode | 不明确的;
    右:NumberBinaryTreeNode | 不明确的;

    构造函数(值:数字){
        this.value = 值;
    }
}
class NumberBinaryTreeNode {
    value: number;
    left: NumberBinaryTreeNode | undefined;
    right: NumberBinaryTreeNode | undefined;

    constructor(value: number) {
        this.value = value;
    }
}

我们将类似地将链表实现为一个或多个节点,每个节点存储一个string和一个对下一个节点的引用,或者undefined如果没有下一个节点,如下一个清单所示。

We will similarly implement the linked list as one or more nodes, each storing a string and a reference to the next node, or undefined if there is no next node, as the next listing shows.

清单 9.12。字符串链表
类 StringLinkedListNode {
    值:字符串;
    下一个:StringLinkedListNode | 不明确的;

    构造函数(值:字符串){
        this.value = 值;
    }
}
class StringLinkedListNode {
    value: string;
    next: StringLinkedListNode | undefined;

    constructor(value: string) {
        this.value = value;
    }
}

现在,如果我们在项目的另一部分需要一个字符串的二叉树怎么办?我们可以实现一个StringBinaryTreeNode与 相同的NumberBinaryTreeNode值类型并将其替换numberstring。这很诱人,因为我们可以复制/粘贴代码并替换一些东西,但复制/粘贴从来都不是一个好的选择。想象一下,我们的类也有一堆方法。如果我们复制/粘贴这些方法,然后在其中一个版本中发现错误,我们很可能会错过修复复制/粘贴版本中的错误。我们确定您知道这是怎么回事:我们可以使用泛型而不是重复!

Now what if we need, in another part of our project, a binary tree of strings? We can implement a StringBinaryTreeNode that is identical to NumberBinaryTreeNode and replace the type of value from number to string. This is tempting, as we can just copy/paste the code and replace a couple of things, but copy/pasting is never a good option. Imagine that our class also has a bunch of methods. If we copied/pasted those methods and then found a bug in one of the versions, we’d likely miss fixing the bug in the copied/pasted version. We’re sure you see where this is going: we can use generics instead of duplication!

9.2.1.通用数据结构

9.2.1. Generic data structures

我们可以实现适用于任何类型的泛型BinaryTreeNode<T>,如下一个清单所示。

We can implement a generic BinaryTreeNode<T> that works for any type, as shown in the next listing.

清单 9.13。通用二叉树
类 BinaryTreeNode<T> {
    值:T;                                1个
    左:BinaryTreeNode<T> | 不明确的;
    右:BinaryTreeNode<T> | 不明确的;

    构造函数(值:T){
        this.value = 值;
    }
}
class BinaryTreeNode<T> {
    value: T;                                1
    left: BinaryTreeNode<T> | undefined;
    right: BinaryTreeNode<T> | undefined;

    constructor(value: T) {
        this.value = value;
    }
}

  • 1 BinaryTreeNode<T> 存储类型 T 的值。
  • 1 A BinaryTreeNode<T> stores a value of type T.

事实上,我们不应该等待新的要求有一个字符串的二叉树:我们原来的NumberBinaryTreeNode实现在二叉树数据结构和类型之间有一个不必要的耦合number。同样,我们可以将 our 替换StringLinkedListNode为通用的LinkedListNode<T>,如以下清单所示。

In fact, we shouldn’t wait for the new requirement to have a binary tree of strings to come in: our original NumberBinaryTreeNode implementation has an unnecessary coupling between the binary tree data structure and the type number. Similarly, we can replace our StringLinkedListNode with a generic LinkedListNode<T>, shown in the following listing.

清单 9.14。通用链表
类 LinkedListNode<T> {
    值:T;
    下一个:LinkedListNode<T> | 不明确的;

    构造函数(值:T){
        this.value = 值;
    }
}
class LinkedListNode<T> {
    value: T;
    next: LinkedListNode<T> | undefined;

    constructor(value: T) {
        this.value = value;
    }
}

请记住,大多数语言的库已经提供了您需要的大部分数据结构(列表、队列、堆栈、集合、字典等)。我们正在研究实现以更好地理解泛型,但最好的办法是根本不编写代码。如果我们可以从库中选择一个通用数据结构,我们就应该这样做。

Do keep in mind that most languages have libraries that already provide most of the data structures you need (lists, queues, stacks, sets, dictionaries, and so on). We’re going over implementations to better understand generics, but the best thing to do is not to write code at all. If we can choose a generic data structure from a library, we should do that.

9.2.2.什么是数据结构?

9.2.2. What is a data structure?

让我们有点哲学性地问“数据结构的本质是什么?” 一个数据结构由三部分组成:

Let’s get a bit philosophical and ask “What is the nature of a data structure?” A data structure consists of three parts:

  • 数据本身——前面示例中树和列表中的number和值。string数据结构包含数据。
  • The data itself—the number and string values in our trees and lists in the preceding example. Data structures contain data.
  • 数据的形状——在我们的二叉树中,数据以分层方式布局,一个元素最多有两个孩子。在我们的列表中,数据是按顺序排列的,一个元素在前一个元素之后。
  • The shape of the data—In our binary tree, the data is laid out in hierarchical fashion, with one element having at most two children. In our list, the data is laid out sequentially, one element coming after the previous one.
  • 一组形状保持操作——例如,我们的数据结构可能会提供这组用于添加或删除元素的操作。在前面的示例中我们没有提供任何此类操作,但是很容易想象,例如,在从链表的中间删除一个元素后,我们仍然希望以链表结束。
  • A set of shape-preserving operations—Our data structure might provide this set of operations for adding or removing an element, for example. We did not provide any such operations in the preceding examples, but it’s easy to imagine how after removing an element from the middle of a linked list, for example, we would still want to end up with a linked list.

这里有两个不同的问题。一个是数据——数据的类型和数据结构实例所持有的实际值。另一个是数据的形状和保形操作。像我们在本节开头看到的那些通用数据结构帮助我们解耦这些问题。通用数据结构处理数据的布局、其形状和任何形状保持操作。二叉树是二叉树,不管它包含的是字符串还是数字。我们可以通过将数据布局的责任转移到独立于实际数据内容的通用数据结构来组件化我们的代码。

There are two separate concerns here. One is the data—the type of the data and the actual value that an instance of the data structure holds. The other is the shape of the data and the shape-preserving operations. Generic data structures like the ones we saw at the beginning of this section help us decouple these concerns. A generic data structure handles the layout of the data, its shape, and any shape-preserving operations. A binary tree is a binary tree regardless of whether it contains strings or numbers. We can componentize our code by moving the responsibility for data layout to generic data structures that are independent of the actual data content.

假设我们有所有这些数据结构,让我们看看如何遍历它们并查看它们的内容。

Assuming that we have all these data structures, let’s look at how we can traverse them and view their content.

9.2.3.练习

9.2.3. Exercises

1个

使用通用的、和方法 实现Stack<T>表示堆栈(后进先出)的数据结构。push()pop()peek()

1

Implement a Stack<T> data structure representing a stack (last-in-first-out) with the common push(), pop(), and peek() methods.

2个

使用和两种类型的成员 实现一个Pair<T, U>数据结构。firstsecond

2

Implement a Pair<T, U> data structure with first and second members of the two types.

9.3. 遍历任何数据结构

9.3. Traversing any data structure

假设我们要按顺序遍历二叉树并打印其所有元素的值,如清单 9.15所示。快速提醒一下,中序遍历是递归遍历左-父-右(图 9.2)。

Let’s say we want to traverse our binary tree in order and print the value of all its elements, as shown in listing 9.15. As a quick reminder, an in-order traversal is the recursive traversal left–parent–right (figure 9.2).

图 9.2。中序遍历。递归地向左走,直到我们到达最左边的节点,转到它的父节点,然后转到右边的节点。接下来,我们回到父节点的父节点,然后转到它的右节点。订单总是留下的;然后,当所有子树都被访问时,父节点;然后对了

清单 9.15。按顺序打印
类 BinaryTreeNode<T> {                                    1
    值:T;
    左:BinaryTreeNode<T> | 不明确的;
    右:BinaryTreeNode<T> | 不明确的;

    构造函数(值:T){
        this.value = 值;
    }
}

函数 printInOrder<T>(root: BinaryTreeNode<T>): void {
    if (root.left != undefined) {                            2
        printInOrder(root.left);
    }

    控制台日志(根值);                                3个

    if (root.right != undefined) {                           4
        printInOrder(root.right);
    }
}
class BinaryTreeNode<T> {                                   1
    value: T;
    left: BinaryTreeNode<T> | undefined;
    right: BinaryTreeNode<T> | undefined;

    constructor(value: T) {
        this.value = value;
    }
}

function printInOrder<T>(root: BinaryTreeNode<T>): void {
    if (root.left != undefined) {                           2
        printInOrder(root.left);
    }

    console.log(root.value);                                3

    if (root.right != undefined) {                          4
        printInOrder(root.right);
    }
}

  • 1 这与我们之前定义的通用二叉树相同。
  • 1 This is the same generic binary tree we defined before.
  • 2 如果存在,我们递归地找到左孩子。
  • 2 We recursively go to the left child if one exists.
  • 3 然后我们打印这个节点的值。
  • 3 Then we print the value of this node.
  • 4 最后,我们递归地找到正确的孩子(如果存在的话)。
  • 4 Finally, we recursively go to the right child if one exists.

作为示例,让我们创建一个包含几个节点的树,并查看printInOrder()以下代码中返回的内容。

As an example, let’s create a tree with a few nodes and see what printInOrder() returns in the following code.

清单 9.16。printInOrder()例子
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

打印订单(根);
let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

printInOrder(root);

这段代码创建了图 9.3所示的树。

This code creates the tree shown in figure 9.3.

图 9.3。二叉树示例

按顺序遍历它会打印

Traversing it in order will print

2个
3个
1个
4个
2
3
1
4

如果我们还想打印字符串链表的所有值怎么办?我们可以实现一个printList()函数,从头到尾遍历列表并打印每个元素,如下一个清单所示。

What if we also want to print all the values of a linked list of strings? We can implement a printList() function that traverses a list from head to tail and prints each element, as the next listing shows.

清单 9.17。打印链表
类 LinkedListNode<T> {                                     1
    值:T;
    下一个:LinkedListNode<T> | 不明确的;

    构造函数(值:T){
        this.value = 值;
    }
}

函数 printLinkedList<T>(head: LinkedListNode<T>): void {
    让当前:LinkedListNode<T> | 未定义=头;       2个

    while (current) {                                         3 
        console.log(current.value);                          4 
        current = current.next;                              4个
    }
}
class LinkedListNode<T> {                                    1
    value: T;
    next: LinkedListNode<T> | undefined;

    constructor(value: T) {
        this.value = value;
    }
}

function printLinkedList<T>(head: LinkedListNode<T>): void {
    let current: LinkedListNode<T> | undefined = head;       2

    while (current) {                                        3
        console.log(current.value);                          4
        current = current.next;                              4
    }
}

  • 1 之前看到的通用链表实现
  • 1 The generic linked list implementation we saw before
  • 2 我们从列表的头部开始。
  • 2 We start from the head of the list.
  • 3 只要我们还有一个节点,我们就重复。
  • 3 We repeat as long as we still have a node.
  • 4 打印节点值,并前进到下一个节点。
  • 4 Print the node value, and advance to the next node.

举一个具体的例子,我们可以初始化一个字符串列表并使用 打印它printLinkedList(),如下面的清单所示。

Taking a concrete example, we can initialize a list of strings and print it by using printLinkedList(), shown in the following listing.

清单 9.18。printLinkedList()例子
让 head: LinkedListNode<string> = new LinkedListNode("Hello");
head.next = new LinkedListNode("世界");
head.next.next = new LinkedListNode("!!!");

打印链表(头);
let head: LinkedListNode<string> = new LinkedListNode("Hello");
head.next = new LinkedListNode("World");
head.next.next = new LinkedListNode("!!!");

printLinkedList(head);

此代码创建如图 9.4所示的列表。

This code creates the list shown in figure 9.4.

图 9.4。链表示例

运行代码将打印

Running the code will print

你好
世界
!!!
Hello
World
!!!

这行得通,但也许有更好的方法。

This works, but maybe there is a better way.

9.3.1.使用迭代器

9.3.1. Using iterators

如果我们可以根据职责进一步拆分代码会怎样?我们的print-InOrder()andprintLinkedList()函数执行两个任务:遍历数据结构并打印其内容。更糟糕的是,第二个任务重叠;这两个函数都打印值。

What if we could further split the code apart based on responsibilities? Our print-InOrder() and printLinkedList() functions perform two tasks: traverse a data structure and print its contents. Even worse, the second task overlaps; both functions print values.

我们可以再做一个概括。让我们将遍历移动到它自己的组件。让我们从我们的二叉树开始。我们需要一种方法来按顺序遍历树中的每个项目并返回每个节点的值。我们可以称这种遍历迭代;我们正在迭代数据结构。

We can make another generalization. Let’s move traversal to its own component. Let’s start with our binary tree. We need a way to go over every item in the tree in order and return the value of each node. We can call this traversal iteration; we are iterating over the data structure.

迭代器

迭代是一种能够遍历数据结构的对象。它提供了一个标准接口,可以对客户端隐藏数据结构的实际形状。

An iterator is an object that enables traversal of a data structure. It provides a standard interface that hides the actual shape of the data structure from the clients.

让我们实现我们的迭代器。我们首先将 an 定义IteratorResult<T>为包含两个属性的类型:一个value类型属性T和一个简单地告诉我们是否已经到达终点的done类型属性boolean,如以下清单所示。

Let’s implement our iterators. We’ll start by defining an IteratorResult<T> as a type that contains two properties: a value property of type T and a done property of type boolean that simply tells us whether we’ve reached the end, as shown in the following listing.

清单 9.19。迭代器结果
类型 IteratorResult<T> = {
    完成:布尔值;
    值:T;
}
type IteratorResult<T> = {
    done: boolean;
    value: T;
}

在下一个清单中,定义一个Iterator<T>声明单个next()方法的迭代器接口。此方法返回一个IteratorResult<T>.

In the next listing, define an iterator interface Iterator<T> that declares a single next() method. This method returns an IteratorResult<T>.

清单 9.20。迭代器接口
接口迭代器<T> {
    下一个():迭代器结果<T>;
}
interface Iterator<T> {
    next(): IteratorResult<T>;
}

现在我们可以将 a 实现BinaryTreeNodeIterator<T>为类实现,如代码清单 9.21Iterator<T>所示。我们正在使用私有方法进行有序遍历,并将所有节点值推送到队列中。该方法通过使用数组方法使值出列并返回值,直到没有更多值可返回(图 9.5)。 inOrder()next()shift()IteratorResult<T>

Now we can implement a BinaryTreeNodeIterator<T> as a class implementing Iterator<T>, as shown in listing 9.21. We’re doing an in-order traversal with the private method inOrder() and pushing all node values to a queue. The next() method dequeues the values by using the array shift() method and returns IteratorResult<T> values until there are no more values to return (figure 9.5).

图 9.5。inOrder()按顺序遍历二叉树并将所有值添加到队列中。next()出列值并在遍历期间返回它们。

清单 9.21。二叉树迭代器
类 BinaryTreeIterator<T> 实现 Iterator<T> {
    私有值:T[];                                      1个
    私有根:BinaryTreeNode<T>;

    构造函数(根:BinaryTreeNode<T>){
        this.values = [];
        this.root = root;

        这个.inOrder(根);                                   2个
    }

    下一个():迭代器结果<T> {
        常量结果:T | undefined = this.values.shift();    3个

        如果(!结果){
            return { done: true, value: this.root.value };    4个
        }

        返回{完成:假,值:结果};
    }

    private inOrder(node: BinaryTreeNode<T>): void {           5
        如果(node.left!=未定义){
            这个.inOrder(node.left);
        }

        this.values.push(node.value);                         6个

        如果(node.right!=未定义){
            this.inOrder(node.right);
        }
    }
}
class BinaryTreeIterator<T> implements Iterator<T> {
    private values: T[];                                      1
    private root: BinaryTreeNode<T>;

    constructor(root: BinaryTreeNode<T>) {
        this.values = [];
        this.root = root;

        this.inOrder(root);                                   2
    }

    next(): IteratorResult<T> {
        const result: T | undefined = this.values.shift();    3

        if (!result) {
            return { done: true, value: this.root.value };    4
        }

        return { done: false, value: result };
    }

    private inOrder(node: BinaryTreeNode<T>): void {          5
        if (node.left != undefined) {
            this.inOrder(node.left);
        }

        this.values.push(node.value);                         6

        if (node.right != undefined) {
            this.inOrder(node.right);
        }
    }
}

  • 1 值队列
  • 1 Queue of values
  • 2 构造函数执行顺序遍历以填充值队列。
  • 2 Constructor performs an in-order traversal to populate the queue of values.
  • 3 每次调用 next() 都会通过调用 shift() 使一个值出列。
  • 3 Each call to next() dequeues a value by calling shift().
  • 4 如果结果未定义,我们将 done 设置为 true 并返回一些默认值。
  • 4 If result is undefined, we set done as true and return some default value.
  • 5 inOrder() 执行中序遍历。
  • 5 inOrder() performs the in-order traversal.
  • 6 我们将每个节点的值添加到值队列中。
  • 6 We add the value of each node to the value queue.

这个实现不是最有效的,因为我们需要一个队列,其元素数量与树中的节点数量相同。我们可以做一个更高效的遍历,需要更少的内存,但逻辑会变得更复杂。让我们现在以此为例,因为我们很快就会看到更好、更简单的方法来做到这一点。

This implementation is not the most efficient, as we need a queue with the same number of elements as the number of nodes in the tree. We can do a more efficient traversal that requires less memory, but the logic gets more complex. Let’s use this for now as an example, as we’ll soon see a better and simpler way to do this.

让我们LinkedListIterator<T>在下一个清单中实现遍历链表。

Let’s also implement the LinkedListIterator<T> to traverse our linked list in the next listing.

清单 9.22。链表迭代器
类 LinkedListIterator<T> 实现 Iterator<T> {
    私有头:LinkedListNode<T>;
    私人电流:LinkedListNode<T> | 不明确的;

    构造函数(头:LinkedListNode<T>){
        this.head = 头;
        this.current = head;
    }

    下一个():迭代器结果<T> {
        如果(!this.current){
            return { done: true, value: this.head.value };    1个
        }

        常量结果:T = this.current.value;                 2 
        this.current = this.current.next;                     3
        返回{完成:假,值:结果};                4个
    }
}
class LinkedListIterator<T> implements Iterator<T> {
    private head: LinkedListNode<T>;
    private current: LinkedListNode<T> | undefined;

    constructor(head: LinkedListNode<T>) {
        this.head = head;
        this.current = head;
    }

    next(): IteratorResult<T> {
        if (!this.current) {
            return { done: true, value: this.head.value };    1
        }

        const result: T = this.current.value;                 2
        this.current = this.current.next;                     3
        return { done: false, value: result };                4
    }
}

  • 1 如果我们已经到达列表的末尾并且 current 未定义,则将 done 设置为 true 并返回一些虚拟值(永远不应该使用)。
  • 1 If we’ve reached the end of the list and current is undefined, set done to true and return some dummy value (which should never be used).
  • 2 result存放的是当前节点的值。
  • 2 result stores the value of the current node.
  • 3 我们将当前节点推进到列表中的下一个节点。
  • 3 We advance the current node to the next node in the list.
  • 4 返回存储的结果。
  • 4 Return the stored result.

完成管道后,让我们看看为什么这些迭代器很有用。如果我们想要打印二叉树中所有节点的值以及字符串链表中所有字符串的值,我们不再需要单独的函数。我们可以使用一个带有迭代器参数的通用函数,该函数使用它来检索要打印的值,如以下代码所示。

With the plumbing out of the way, let’s see why these iterators are useful. If we want to print the values of all the nodes in a binary tree and all the strings in a linked list of strings, we no longer need separate functions. We can use a single common function that takes an iterator argument, which uses it to retrieve the values to print, as shown in the following code.

清单 9.23。print()使用迭代器
function print<T>(iterator: Iterator<T>): void {         1
    让结果:IteratorResult<T> = iterator.next();    2个

    while (!result.done) {                               3 
        console.log(result.value);                      3
        结果 = iterator.next();                       3个
    }
}
function print<T>(iterator: Iterator<T>): void {        1
    let result: IteratorResult<T> = iterator.next();    2

    while (!result.done) {                              3
        console.log(result.value);                      3
        result = iterator.next();                       3
    }
}

  • 1 print() 是一个将迭代器作为参数的通用函数。
  • 1 print() is a generic function that takes an iterator as argument.
  • 2 我们使用 next() 进行初始化,提取第一个值。
  • 2 We initialize with next(), pulling the first value.
  • 3 虽然结果没有返回 done 为真,我们可以打印值并推进迭代器。
  • 3 Although result doesn’t return done as true, we can print the value and advance the iterator.

因为print()使用迭代器,我们可以将 aBinaryTree-Iterator<T>或 a传递给它LinkedListIterator<T>。事实上,只要我们有一个可以遍历该数据结构的迭代器,我们就可以用它来打印任何数据结构。

Because print() works with iterators, we can pass to it either a BinaryTree-Iterator<T> or a LinkedListIterator<T>. In fact, we can use it to print any data structure as long as we have an iterator that can traverse that data structure.

使用迭代器,我们可以重用更多的代码。例如,如果我们需要一种方法来确定某个值是否存在于数据结构中,我们不需要为每个数据结构实现单独的函数;我们可以简单地实现一个contains()函数,它接受一个迭代器和一个要查找的值,如下一个清单所示,然后我们可以将它与实现该Iterator<T>接口的任何迭代器一起使用(图 9.6)。

With iterators, we can reuse a lot more code. If we need a way to determine whether a certain value exists in a data structure, for example, we don’t need to implement a separate function for each data structure; we can simply implement a contains() function that takes an iterator and a value to look for, as shown in the next listing, and then we can use it with any iterator that implements the Iterator<T> interface (figure 9.6).

图 9.6。BinaryTreeIterator实现二叉树遍历。LinkedListIterator实现链表遍历。双方都履行Iterator合同。print()contains()接受一个Iterator作为参数,这样我们就可以混合和匹配具有不同数据结构的函数。

清单 9.24。contains()使用迭代器
函数包含<T>(值:T,迭代器:迭代器<T>):布尔值{
    让结果:IteratorResult<T> = iterator.next();

    而(!result.done){
        if (result.value == value) 返回真;

        结果 = iterator.next();
    }

    返回假;
}
function contains<T>(value: T, iterator: Iterator<T>): boolean {
    let result: IteratorResult<T> = iterator.next();

    while (!result.done) {
        if (result.value == value) return true;

        result = iterator.next();
    }

    return false;
}

迭代器是连接数据结构和算法的粘合剂,可以实现这种解耦。通过这种方法,如果它们之间的接口是 . ,我们可以混合和匹配具有不同功能的不同数据结构Iterator<T>

Iterators are the glue that connects data structures and algorithms, enabling this decoupling. With this approach, we can mix and match different data structures with different functions if the interface between them is Iterator<T>.

请注意,数据结构可能具有不同的遍历。我们重点介绍了二叉树的中序遍历,但也有前序遍历和后序遍历。我们可以将所有这些遍历实现为同一二叉树上的迭代器。遍历策略和数据结构之间不一定存在一一对应关系。

Note that a data structure may have different traversals. We’ve focused on an in-order traversal of a binary tree, but there are also pre-order and post-order traversals. We can implement all these traversals as iterators over the same binary tree. A one-to-one correspondence doesn’t have to exist between traversal strategies and data structures.

9.3.2.精简迭代代码

9.3.2. Streamlining iteration code

迭代器非常有用,以至于大多数主流语言都为它们提供了库支持,在许多情况下,甚至还提供了特殊的语法。我们在第 6 章讨论生成器时简要地谈到了这个主题,我们将在这里展开。

Iterators are so useful that most mainstream languages provide library support for them and, in many cases, even special syntax. We briefly touched on this topic in chapter 6, when we looked at generators, and we’ll expand on it here.

我们真的不必定义IteratorResult<T>Iterator<T>类型;TypeScript 已经预定义了它们。在 C# 中,等效接口是IEnumerator<T>,它同样支持数据结构的遍历。Java 等价物也被命名为Iterator<T>. C++ 库使用多种迭代器。我们将在第 10 章讨论迭代器类别时详细讨论这些类别。这里的关键要点是这种模式非常有用,它具有开箱即用的支持。

We didn’t really have to define the IteratorResult<T> and Iterator<T> types; TypeScript has them predefined. In C#, the equivalent interface is IEnumerator<T>, which similarly enables traversal of data structures. The Java equivalent is also named Iterator<T>. The C++ library works with several kinds of iterators. We’ll talk more about these categories in chapter 10, when we talk about iterator categories. The key takeaway here is that this pattern is so useful that it has out-of-the-box support.

迭代器实现了遍历数据结构的代码,而另一个接口让我们将类型标记为可以迭代的东西:接口Iterable<T>,定义如下。

Whereas iterators implement the code to traverse a data structure, another interface lets us mark a type as something that can be iterated over: the Iterable<T> interface, defined as follows.

清单 9.25。可迭代接口
接口可迭代<T> {
    [Symbol.iterator](): Iterator<T>;
}
interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

[Symbol.iterator]是一些特定于 TypeScript 的语法。它只是意味着一个特殊的名字,非常像我们在整本书中用来实现名义子类型的符号技巧。该Iterable<T>接口声明了一个名为的方法[Symbol.iterator](),该方法返回一个Iterator<T>.

The [Symbol.iterator] is a bit of TypeScript-specific syntax. It just means a special name, very much like the symbol trick we used to implement nominal subtyping throughout the book. The Iterable<T> interface declares a method named [Symbol.iterator]() that returns an Iterator<T>.

让我们更新我们的LinkedListNode<T>类型并使其在下一个清单中可迭代。

Let’s update our LinkedListNode<T> type and make it iterable in the next listing.

清单 9.26。可迭代链表
类 LinkedListNode<T>实现 Iterable<T> {
    值:T;
    下一个:LinkedListNode<T> | 不明确的;

    构造函数(值:T){
        this.value = 值;
    }

    [Symbol.iterator](): Iterator<T> { 
        return new LinkedListIterator<T>(this);        1
     } 
}
class LinkedListNode<T> implements Iterable<T> {
    value: T;
    next: LinkedListNode<T> | undefined;

    constructor(value: T) {
        this.value = value;
    }

    [Symbol.iterator](): Iterator<T> {
        return new LinkedListIterator<T>(this);       1
    }
}

  • 1 我们通过在此列表上创建 LinkedListIterator 的新实例来实现 Iterable<T> 接口。
  • 1 We implement the Iterable<T> interface by creating a new instance of LinkedListIterator on this list.

[Symbol- .iterator]()我们还可以通过提供一个类似的方法来将我们的二叉树标记为可迭代的BinaryTreeIterator<T>

We can also mark our binary tree as iterable by providing a similar [Symbol- .iterator]() method that creates a BinaryTreeIterator<T>.

Iterables 允许我们使用for ... ofTypeScript 中的语法。这种语法是用于遍历可迭代对象的所有元素的特殊语法,使我们的代码更加简洁。大多数主流语言都有一个等价物。C# 有IEnumerable<T>, IEnumerator<T>, 和foreach循环。Java 有Iterable<T>, Iterator<T>, 和for :循环。

Iterables allow us to use the for ... of syntax in TypeScript. This syntax is special syntax for iterating over all elements of an iterable and makes our code much cleaner. Most mainstream languages have an equivalent. C# has IEnumerable<T>, IEnumerator<T>, and foreach loops. Java has Iterable<T>, Iterator<T>, and for : loops.

让我们快速回顾一下下一个清单中的print()和实现,然后更新它们以使用可迭代对象和代替。 contains()for ... of

Let’s quickly review the print() and contains() implementations in the next listing and then update them to use iterables and for ... of instead.

清单 9.27。print()contains()带有 Iterator 参数
函数打印<T>(迭代器:迭代器<T>):void {
    让结果:IteratorResult<T> = iterator.next();

    而(!result.done){
        控制台日志(结果。值);
        结果 = iterator.next();
    }
}

函数包含<T>(值:T,迭代器:迭代器<T>):布尔值{
    让结果:IteratorResult<T> = iterator.next();

    而(!result.done){
        if (result.value == value) 返回真;

        结果 = iterator.next();
    }

    返回假;
}
function print<T>(iterator: Iterator<T>): void {
    let result: IteratorResult<T> = iterator.next();

    while (!result.done) {
        console.log(result.value);
        result = iterator.next();
    }
}

function contains<T>(value: T, iterator: Iterator<T>): boolean {
    let result: IteratorResult<T> = iterator.next();

    while (!result.done) {
        if (result.value == value) return true;

        result = iterator.next();
    }

    return false;
}

在下一个清单中,我们将更新函数以采用Iterable<T>参数而不是。始终可以通过调用方法从 an 中获取Iterator-<T>an 。 Iterator<T>Iterable<T>[Symbol.iterator]()

We’ll update the functions to take an Iterable<T> argument instead of an Iterator-<T> in the next listing. An Iterator<T> can always be obtained from an Iterable<T> by calling the [Symbol.iterator]() method.

清单 9.28。print()contains()带有 Iterable 参数
function print<T>(iterable: Iterable<T>): void {
    对于(可迭代的常量项){                                1
        控制台日志(项目);
    }
}

函数包含<T>(值:T,可迭代:Iterable<T>):布尔值{
    对于(可迭代的常量项){                                2
        如果(项目==值)返回真;
    }

    返回假;
}
function print<T>(iterable: Iterable<T>): void {
    for (const item of iterable) {                               1
        console.log(item);
    }
}

function contains<T>(value: T, iterable: Iterable<T>): boolean {
    for (const item of iterable) {                               2
        if (item == value) return true;
    }

    return false;
}

  • 1 print() 使用 for...of 循环将每个元素打印到控制台。
  • 1 print() uses a for...of loop to print each element to the console.
  • 2 contains() 使用 for...of 循环将每个元素与给定值进行比较。
  • 2 contains() uses a for...of loop to compare each element to the given value.

如我们所见,代码更加简洁。Iterator<T>不用使用and手动遍历我们的数据结构next(),我们可以使用一个使用for...of.

As we can see, the code is much more succinct. Instead of iterating over our data structures manually, using an Iterator<T> and next(), we can do it with a one-liner that uses for...of.

现在让我们看看如何简化我们的迭代器代码。我们说过我们的有序二叉树遍历是低效的,因为它在返回之前对所有节点进行排队。一个更有效的解决方案是遍历树而不对所有节点进行排队,但实现会变得有点复杂。以下是我们目前使用的实现。

Now let’s see how we can simplify our iterator code. We said that our in-order binary tree traversal is inefficient, as it queues all the nodes before returning them. A more efficient solution would traverse the tree without queuing all nodes, but the implementation would get a bit more complex. Following is the implementation we’ve used so far.

清单 9.29。二叉树迭代器
类 BinaryTreeIterator<T> 实现 Iterator<T> {
    私有值:T[];
    私有根:BinaryTreeNode<T>;

    构造函数(根:BinaryTreeNode<T>){
        this.values = [];
        this.root = root;

        这个.inOrder(根);
    }

    下一个():迭代器结果<T> {
        常量结果:T | undefined = this.values.shift();

        如果(!结果){
            return { done: true, value: this.root.value };
        }

        返回{完成:假,值:结果};
    }

    private inOrder(node: BinaryTreeNode<T>): void {
        如果(node.left!=未定义){
            这个.inOrder(node.left);
        }

        this.values.push(node.value);

        如果(node.right!=未定义){
            this.inOrder(node.right);
        }
    }
}
class BinaryTreeIterator<T> implements Iterator<T> {
    private values: T[];
    private root: BinaryTreeNode<T>;

    constructor(root: BinaryTreeNode<T>) {
        this.values = [];
        this.root = root;

        this.inOrder(root);
    }

    next(): IteratorResult<T> {
        const result: T | undefined = this.values.shift();

        if (!result) {
            return { done: true, value: this.root.value };
        }

        return { done: false, value: result };
    }

    private inOrder(node: BinaryTreeNode<T>): void {
        if (node.left != undefined) {
            this.inOrder(node.left);
        }

        this.values.push(node.value);

        if (node.right != undefined) {
            this.inOrder(node.right);
        }
    }
}

我们能做的就是用生成器替换这段代码。(我们在第 6 章中简要讨论了生成器。)生成器是一个可恢复的函数,它使用一条yield语句返回,并在再次调用时从它停止的地方恢复执行。TypeScript 中的生成器返回一个IterableIterator<T>,它只是我们了解的两个接口的组合:Iterable<T>Iterator<T>。实现两者的对象可以“手动”迭代,next()但也可以在语句中使用for...of

What we can do is replace this code with a generator. (We briefly talked about generators in chapter 6.) A generator is a resumable function that returns using a yield statement and, when called again, resumes execution from where it left off. Generators in TypeScript return an IterableIterator<T>, which is simply a combination of the two interfaces we’ve learned about: Iterable<T> and Iterator<T>. An object that implements both can be iterated over “manually” with next() but can also be used in a for...of statement.

让我们在代码清单 9.30中将我们的二叉树遍历重新实现为一个生成器。使用生成器,我们可以递归地实现遍历并不断产生值,直到我们遍历整个数据结构(图 9.7)。

Let’s reimplement our binary tree traversal as a generator in listing 9.30. With generators, we can implement traversal recursively and keep yielding values until we’ve gone over the whole data structure (figure 9.7).

图 9.7。inOrderIterator()是一个生成器,所以它返回一个IterableIterator<T>. 与 一样inOrder(),此函数递归地遍历树,但不是对项目进行排队,而是产生它们。调用next()返回的迭代器恢复生成器并产生下一个值。

清单 9.30。使用生成器的二叉树迭代器
函数* inOrderIterator<T>(根:BinaryTreeNode<T>):      1
    IterableIterator<T> {
    如果(根。左){
        for (inOrderIterator(root.left) 的常量值) {
            屈服值;                                   2个
        }
    }

    产生根值;                                      3个

    如果(root.right){
        for (inOrderIterator(root.right) 的常量值) {
            屈服值;                                   4个
        }
    }
}
function* inOrderIterator<T>(root: BinaryTreeNode<T>):     1
    IterableIterator<T> {
    if (root.left) {
        for (const value of inOrderIterator(root.left)) {
            yield value;                                   2
        }
    }

    yield root.value;                                      3

    if (root.right) {
        for (const value of inOrderIterator(root.right)) {
            yield value;                                   4
        }
    }
}

  • 1 function* 将此函数定义为生成器,因此它可以 yield 和 resume。
  • 1 function* defines this function as a generator, so it can yield and resume.
  • 2 首先遍历左子树,yield所有返回值。
  • 2 First, traverse the left subtree and yield all returned values.
  • 3 然后产生当前值。
  • 3 Then yield the current value.
  • 4 然后遍历右子树,yield所有返回值。
  • 4 Then traverse the right subtree and yield all returned values.

这个实现要简洁得多。注意inOrderIterator()是递归的。在每个级别,值都“向上”产生,直到它们传播到原始调用者。

This implementation is much more succinct. Note that inOrderIterator() is recursive. At each level, values are yielded “up” until they propagate to the original caller.

同样,我们可以用一个生成器遍历我们的链表,简化逻辑。我们最初的实现看起来像下面的清单。

Similarly, we can traverse our linked list with a generator, simplifying the logic. Our original implementation looked like the following listing.

清单 9.31。链表迭代器
类 LinkedListIterator<T> 实现 Iterator<T> {
    私有头:LinkedListNode<T>;
    私人电流:LinkedListNode<T> | 不明确的;

    构造函数(头:LinkedListNode<T>){
        this.head = 头;
        this.current = head;
    }

    下一个():迭代器结果<T> {
        如果(!this.current){
            return { done: true, value: this.head.value };
        }

        常量结果:T = this.current.value;
        this.current = this.current.next;
        返回{完成:假,值:结果};
    }
}
class LinkedListIterator<T> implements Iterator<T> {
    private head: LinkedListNode<T>;
    private current: LinkedListNode<T> | undefined;

    constructor(head: LinkedListNode<T>) {
        this.head = head;
        this.current = head;
    }

    next(): IteratorResult<T> {
        if (!this.current) {
            return { done: true, value: this.head.value };
        }

        const result: T = this.current.value;
        this.current = this.current.next;
        return { done: false, value: result };
    }
}

我们可以将其替换为另一个在遍历列表时产生值的生成器,如以下清单所示。

We can replace this with another generator that yields values as it traverses the list, as shown in the following listing.

清单 9.32。使用生成器的链表迭代器
函数* linkedListIterator<T>(头:LinkedListNode<T>):
    IterableIterator<T> {
    让当前:LinkedListNode<T> | 未定义=头;

    而(当前){
        产生当前值;        1个
        当前=当前.下一个;
    }
}
function* linkedListIterator<T>(head: LinkedListNode<T>):
    IterableIterator<T> {
    let current: LinkedListNode<T> | undefined = head;

    while (current) {
        yield current.value;        1
        current = current.next;
    }
}

  • 1 我们在遍历链表时产生每个值。
  • 1 We yield each value as we traverse the linked list.

编译器将其转换为一个迭代器,该迭代器提供IteratorResult<T>每个 的值yield。当函数到达末尾并退出(没有产生值)时,返回 一个设置为 的IteratorResult<T>final 。donetrue

The compiler translates this into an iterator that provides IteratorResult<T> values from each yield. When the function reaches the end and exits (without yielding a value), a final IteratorResult<T> with done set to true is returned.

最后一步是将这些生成器作为[Symbol.iterator](). 让我们看看链表的最终版本是什么样的。

The final step is plugging these generators into the data structures themselves as implementations of [Symbol.iterator](). Let’s see what our final version of the linked list looks like.

清单 9.33。使用生成器的可迭代链表
类 LinkedListNode<T> 实现 Iterable<T> {
    值:T;
    下一个:LinkedListNode<T> | 不明确的;

    构造函数(值:T){
        this.value = 值;
    }

    [Symbol.iterator](): Iterator<T> {
        返回链接列表迭代器(这个);       1个
    }
}
class LinkedListNode<T> implements Iterable<T> {
    value: T;
    next: LinkedListNode<T> | undefined;

    constructor(value: T) {
        this.value = value;
    }

    [Symbol.iterator](): Iterator<T> {
        return linkedListIterator(this);       1
    }
}

  • 1 [Symbol.iterator]() 简单地返回 linkedListIterator() 的结果。
  • 1 [Symbol.iterator]() simply returns the result of linkedListIterator().

这是有效的,因为生成器返回一个IterableIterator<T>. 有时我们需要 anIterable<T>以便我们可以在循环中嵌入对生成器的调用for...of(例如,for (const value of linkedListIterator(...))。有时我们需要一个Iterator<T>代替,如前面的示例,因此我们可以for...of在数据结构本身的实例上使用循环。

This works because the generator returns an IterableIterator<T>. Sometimes we want an Iterable<T> so we can embed a call to the generator inside a for...of loop (for example, for (const value of linkedListIterator(...)). Sometimes we want an Iterator<T> instead, as in the preceding example, so we can use a for...of loop on an instance of the data structure itself.

9.3.3.迭代器回顾

9.3.3. Iterators recap

我们从处理数据形状的几个通用数据结构开始,不管数据是什么。我们看到这种抽象很强大。但是,如果我们编写代码来遍历每个数据结构,每当我们想要对其应用操作时,例如print()or contains(),我们最终会得到每个函数的多个版本。

We started with a couple of generic data structures that took care of the shape of the data, regardless of what that data was. We saw that this abstraction is powerful. But if we write code to traverse each data structure whenever we want to apply an operation over it, such as print() or contains(), we end up with multiple versions of each function.

Enter Iterator<T>,一个通过使用next(). 这个接口允许我们编写一个单一版本的print()和一个单一版本的contains(),都在迭代器上运行。

Enter Iterator<T>, an interface that decouples the shape of the data from the functions by providing a unified traversal interface using next(). This interface allows us to write a single version of print() and a single version of contains(), both operating on iterators.

但是,通过调用next()和检查进行迭代done仍然很麻烦。原来Iterable<T>是一个声明[Symbol.iterator]()方法的接口。我们可以使用此方法来获取迭代器。Iterable<T>更好的是,我们可以在声明中添加一个for...of。这种语法不仅更简洁,而且我们永远不必显式地处理迭代器,因为在循环的每次迭代中,我们都会得到实际的元素。

Iterating by calling next() and checking done is still cumbersome, though. Turns out Iterable<T> is an interface that declares a [Symbol.iterator]() method. We can use this method to get an iterator. Better yet, we can put an Iterable<T> in a for...of statement. Not only is this syntax cleaner, but we also never have to deal with the iterator explicitly, as on each iteration of the loop, we get the actual element.

最后,我们看到如果我们使用一个在遍历数据结构时产生值的生成器,我们可以简化遍历代码。生成器返回一个Iterable-Iterator<T>,因此我们可以直接在循环内使用它们for...of或实现数据结构的Iterable<T>接口。

Finally, we saw that we can simplify the traversal code if we use a generator that yields values as it traverses the data structure. Generators return an Iterable-Iterator<T>, so we can use them both directly inside for...of loops or to implement a data structure’s Iterable<T> interface.

如前所述,大多数主流编程语言都有一个等效的特殊类型,可以实现for遍历元素的循环。至于生成器,虽然 Java 缺少内置yield语句,但 C# 支持它们,使用与 TypeScript 非常相似的语法。

As mentioned earlier, most mainstream programming languages have an equivalent special type that enables a for loop that traverses over elements. As for generators, although Java lacks a built-in yield statement, C# supports them, using a syntax very similar to TypeScript’s.

通常,在定义数据结构时,确保它实现了Iterable<T>. 避免编写嵌入特定数据结构遍历的函数;相反,让它们与迭代器一起工作,以便可以对不同的数据结构重用相同的逻辑。yield在实现遍历逻辑时 考虑一下,因为它通常会使代码更干净、更简洁。

In general, when defining a data structure, make sure that it implements Iterable<T>. Avoid writing functions that embed traversal of one particular data structure; rather, have them work with iterators so that the same logic can be reused with different data structures. Consider yield when implementing the traversal logic, as it usually makes code cleaner and more concise.

更好的 IteratorResult<T>

不幸的是,我们必须使用IteratorResult<T>. 作为返回类型next()。这就是在 TypeScript 中开箱即用地定义接口的方式。这违反了我们在第 3 章中概述的从函数返回结果或错误而不是两者的原则。IteratorResult<T>包含一个boolean属性done和一个value类型的属性T。当迭代器遍历整个列表时,它返回doneastrue但还需要返回一些东西 for value。这value必须是一些默认值,因为它是强制性的,但数据结构已被完全遍历。调用代码绝不意味着使用 valueif doneis true。不幸的是,没有办法强制执行此规则。

It’s unfortunate that we have to use IteratorResult<T> as the return type of next(). This is how the interface is defined out of the box in TypeScript. It goes against the principle we outlined in chapter 3 to return result or error from a function as opposed to both. IteratorResult<T> contains a boolean property done and a value property of type T. When the iterator has traversed the whole list, it returns done as true but also needs to return something for value. This value must be some default, as it is mandatory, but the data structure was fully traversed. Calling code is never meant to use value if done is true. Unfortunately, there is no way to enforce this rule.

更好的合同是总和类型,例如Optional<T>T | undefinedT只要值可用, 这就会返回s ,然后在遍历完成时返回任何内容。

A better contract would be a sum type such as Optional<T> or T | undefined. This will return Ts as long as values are available and then nothing when traversal is finished.

9.3.4.练习

9.3.4. Exercises

1个

为通用二叉树实现预序遍历。前序遍历先是父树,再是左子树,再是右子树。尝试用生成器实现它。

1

Implement a pre-order traversal for a generic binary tree. Pre-order traversal is parent first, followed by left subtree and then right subtree. Try implementing it with a generator.

2个

实现一个向后(从后到前)迭代数组的函数。

2

Implement a function that iterates over an array backward (from back to front).

9.4. 流数据

9.4. Streaming data

在最后一节中,我们将了解迭代器的一个非常有趣的方面:​​它们不一定是有限的。在下一个清单中,让我们实现一个生成无限随机数流的函数。我们将调用它generateRandomNumbers()并让它从无限循环中产生这些数字。

In this last section, we will look at a very interesting aspect of iterators: the fact that they don’t necessarily have to be finite. In the next listing, let’s implement a function that generates an infinite stream of random numbers. We’ll call it generateRandomNumbers() and have it yield these numbers from an infinite loop.

清单 9.34。无限的随机数流
函数* generateRandomNumbers(): IterableIterator<number> {
    while (true) {                                             1 
        yield Math.random();                                  2个
    }
}
function* generateRandomNumbers(): IterableIterator<number> {
    while (true) {                                            1
        yield Math.random();                                  2
    }
}

  • 1 永远循环。
  • 1 Loop forever.
  • 2 在每一步产生一个随机数。
  • 2 Yields a random number at each step.

我们可以调用此函数来获取 an IterableIterator<T>,然后next()多次调用它来获取随机数,如以下清单所示。

We can call this function to get an IterableIterator<T> and then call next() on it a few times to get random numbers, as shown in the following listing.

清单 9.35。使用流中的值
让 iter: IterableIterator<number> = generateRandomNumbers();

console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
let iter: IterableIterator<number> = generateRandomNumbers();

console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);

现实生活中有很多无限数据流的例子:从键盘读取字符、通过网络连接接收数据、收集传感器数据等等。我们可以使用管道来处理这些数据。

There are many examples of infinite streams of data in real life: reading characters from the keyboard, receiving data over a network connection, collecting sensor data, and so on. We can process such data by using pipelines.

9.4.1.加工流水线

9.4.1. Processing pipelines

处理管道的组件是将迭代器作为参数、进行一些处理并返回迭代器的函数。这些功能可以链接在一起以在数据到达时对其进行处理。这种模式在函数式编程语言中很常见,也是反应式编程的基础。

The components of processing pipelines are functions that take an iterator as argument, do some processing, and return an iterator. Such functions can be chained together to process data as it arrives. This pattern is common in functional programming languages and the basis of reactive programming.

例如,让我们实现一个square()函数,对其输入迭代器的所有数字进行平方。我们可以使用一个生成器轻松地做到这一点,该生成器接受一个Iterable- <number>参数并生成其值的平方,如清单 9.36所示。请注意,我们不需要IterableIterator<number>as 输入——只需一个Iterable- <number>——但传入一个就可以了,因为 anIterableIterator<number>也满足Iterable<number>接口。

As an example, let’s implement a square() function that squares all numbers of its input iterator. We can do this easily with a generator that takes an Iterable- <number> argument and yields squares of its values, as shown in listing 9.36. Note that we don’t need an IterableIterator<number> as input—just an Iterable- <number>—but passing one in will work, as an IterableIterator<number> also satisfies the Iterable<number> interface.

清单 9.36。square()
function* square(iter: Iterable<number>):     1 
    IterableIterator<number> {                1
    for (iter 的常量值) {
        屈服值** 2;
    }
}
function* square(iter: Iterable<number>):    1
    IterableIterator<number> {               1
    for (const value of iter) {
        yield value ** 2;
    }
}

  • 1 该函数接受一个 Iterable<number> 并返回一个 IterableIterator<number>。
  • 1 The function takes an Iterable<number> and returns an IterableIterator<number>.

处理管道中的一个常见函数是take(),该函数获取n其输入迭代器的第一个元素并返回它们,丢弃其余元素,如以下代码所示。

A common function in processing pipeline is take(), a function that takes the first n elements of its input iterator and returns them, discarding the rest, as shown in the following code.

清单 9.37。take()
函数* take<T>(iter: Iterable<T>, n: number):
    IterableIterator<T> {
    for (iter 的常量值) {
        如果 (n-- <= 0) 返回;      1个

        屈服值;               2个
    }
}
function* take<T>(iter: Iterable<T>, n: number):
    IterableIterator<T> {
    for (const value of iter) {
        if (n-- <= 0) return;      1

        yield value;               2
    }
}

  • 1 我们递减 n 并在产生 n 个值时停止。
  • 1 We decrement n and stop when we’ve yielded n values.
  • 2 产生一个值。
  • 2 Yield one value.

现在让我们在清单 9.38中创建一个管道,它对无限流中的数字进行平方并获取前五个结果,我们将其打印到控制台(图 9.8)。

Now let’s create a pipeline in listing 9.38 that squares numbers from an infinite stream and takes the first five results, which we print to the console (figure 9.8).

图 9.8。流水线和调用顺序。从的迭代器take()请求一个值。从的迭代器请求一个值。产生一个值到。产生一个值到。 square()square()generateRandomNumber()generateRandomNumbers()square()square()take()

清单 9.38。管道
常量值:IterableIterator<number> =
    采取(广场(generateRandomNumbers()),5);      1个

对于(值的常量值){
    控制台日志(值);
}
const values: IterableIterator<number> =
    take(square(generateRandomNumbers()), 5);      1

for (const value of values) {
    console.log(value);
}

  • 1 take() 从 square 中获取五个值,后者从 generateRandomNumbers() 中获取值。
  • 1 take() takes five values from square, which takes values from generateRandomNumbers().

迭代器是创建此类管道的关键,因为它们支持对值进行逐一处理。同样重要的是要了解这些管道是惰性评估的。在此清单的示例中,values是一个IterableIterator<number>. 尽管它是通过调用我们的管道创建的,但尚未执行任何代码。只有当我们开始在for...of循环中使用值时,值才会开始流动。

Iterators are the key to creating this type of pipeline, as they enable one-by-one processing of values. It’s also important to understand that these pipelines are evaluated lazily. In our example in this listing, values is an IterableIterator<number>. Even though it is created by calling our pipeline, none of the code is executed yet. Only when we start consuming values in the for...of loop do values start flowing through.

在循环的一次迭代中,在迭代器next()上调用values,它调用take(). take()需要一个值,因此它依次调用square(). 同样,square()需要一个值来平方,所以它调用generateRandomNumbers(). generateRandom-Numbers()产生一个随机值到square(),将它平方并产生它到take()take()将它产生到循环中,在那里它被打印到控制台。

In one iteration of the loop, next() is called on the values iterator, which invokes take(). take() needs a value, so it in turn calls square(). Similarly, square() needs a value to square, so it calls generateRandomNumbers(). generateRandom-Numbers() yields a random value to square(), which squares it and yields it to take(). take() yields it to the loop, where it is printed to the console.

因为管道是惰性评估的,所以我们可以使用无限生成器,例如generateRandomNumbers(). 我们将在第 10 章更深入地介绍算法。

Because pipelines are evaluated lazily, we can work with infinite generators, such as generateRandomNumbers(). We’ll cover algorithms in more depth in chapter 10.

9.4.2.练习

9.4.2. Exercises

1个

drop()是另一个常用函数。此函数与 相反take(),因为它丢弃迭代器的第一个n元素并返回其余元素。实施drop()

1

drop() is another common function. This function is the opposite of take(), as it discards the first n elements of an iterator and returns the rest. Implement drop().

2个

创建一个管道,给定一个迭代器,返回第六、第七、第八、第九和第十个元素。drop()提示:这可以通过和的组合来完成take()

2

Create a pipeline that, given an iterator, returns the sixth, seventh, eighth, ninth, and tenth elements. Hint: this can be done with a combination of drop() and take().

概括

Summary

  • 泛型对于分离独立的关注点很有用。
  • Generics are useful for separating independent concerns.
  • 通用数据结构负责数据的形状,无论数据是什么。
  • Generic data structures are responsible for the shape of the data, regardless of what that data is.
  • 迭代器为遍历数据结构提供了一个通用接口。
  • Iterators provide a common interface for traversing data structures.
  • Iterator<T>代表一个迭代器,而Iterable<T>代表可以迭代的东西。
  • Iterator<T> represents an iterator, whereas Iterable<T> represents something that can be iterated over.
  • 迭代器可以通过使用生成器来实现。
  • Iterators can be implemented by using generators.
  • 大多数编程语言都有迭代器和特殊语法来循环它们。
  • Most programming languages have iterators and special syntax to loop over them.
  • 迭代器不必是有限的:它们可以永远产生值。
  • Iterators don’t have to be finite: they can produce values forever.
  • 使用接受和返回迭代器的函数,我们可以构建处理管道。
  • Using functions that take and return iterators, we can build processing pipelines.

现在我们已经介绍了通用数据结构,第 10 章将着眼于编程的其他主要成分:算法。

Now that we’ve covered generic data structures, chapter 10 looks at the other main ingredients of programing: algorithms.

习题答案

Answers to exercises

解耦问题

Decoupling concerns

1个

一个可能的实现:

类框 <T> {
    只读值:T;

    构造函数(值:T){
        this.value = 值;
    }
}

1

A possible implementation:

class Box<T> {
    readonly value: T;

    constructor(value: T) {
        this.value = value;
    }
}

2个

一个可能的实现:

函数 unbox<T>(boxed: Box<T>): T {
    返回装箱值;
}

2

A possible implementation:

function unbox<T>(boxed: Box<T>): T {
    return boxed.value;
}

 

 

通用数据布局

Generic data layout

1个

一个由数组支持的可能实现(在 JavaScript 中,数组自带pop()push()开箱即用):

类堆栈<T> {
    私有值:T[] = [];

    公共推送(值:T){
        this.values.push(值);
    }

    公共 pop(): T {
        如果(this.values.length == 0)抛出错误();

        返回 this.values.pop();
    }

    公众偷看():T {
        如果(this.values.length == 0)抛出错误();

        返回 this.values[this.values.length - 1];
    }
}

1

A possible implementation backed by an array (in JavaScript, arrays come with pop() and push() out of the box):

class Stack<T> {
    private values: T[] = [];

    public push(value: T) {
        this.values.push(value);
    }

    public pop(): T {
        if (this.values.length == 0) throw Error();

        return this.values.pop();
    }

    public peek(): T {
        if (this.values.length == 0) throw Error();

        return this.values[this.values.length - 1];
    }
}

2个

一个可能的实现:

类对 <T, U> {
    只读优先:T;
    只读第二个:U;

    构造函数(第一:T,第二:U){
        this.first = 第一;
        this.second = 第二个;
    }
}

2

A possible implementation:

class Pair<T, U> {
    readonly first: T;
    readonly second: U;

    constructor(first: T, second: U) {
        this.first = first;
        this.second = second;
    }
}

 

 

遍历任何数据结构

Traversing any data structure

1个

此实现与有序实现非常相似;我们只是root.value在产生左子树之前产生:

函数* preOrderIterator<T>(根:BinaryTreeNode<T>):
    IterableIterator<T> {
    产生根值;

    如果(根。左){
        for (preOrderIterator(root.left) 的常量值) {
            屈服值;
        }
    }

    如果(root.right){
        for (preOrderIterator(root.right) 的常量值) {
            屈服值;
        }
    }
}

1

This implementation is very similar to the in-order one; we just yield root.value before we yield the left subtree:

function* preOrderIterator<T>(root: BinaryTreeNode<T>):
    IterableIterator<T> {
    yield root.value;

    if (root.left) {
        for (const value of preOrderIterator(root.left)) {
            yield value;
        }
    }

    if (root.right) {
        for (const value of preOrderIterator(root.right)) {
            yield value;
        }
    }
}

2个

此实现确实使用for循环向后遍历数组,因此调用者不必这样做。

函数* backwardsArrayIterator<T>(array: T[]): IterableIterator<T> {
    for (let i = array.length - 1; i >= 0; i--) {
        产量数组[i];
    }
}

2

This implementation does use a for loop to traverse the array backward so callers don’t have to.

function* backwardsArrayIterator<T>(array: T[]): IterableIterator<T> {
    for (let i = array.length - 1; i >= 0; i--) {
        yield array[i];
    }
}

 

 

流数据

Streaming data

1个

一个可能的实现:

函数* drop<T>(iter: Iterable<T>, n: number):
    IterableIterator<T> {
    for (iter 的常量值) {
        如果 (n-- > 0) 继续;

        屈服值;
    }
}

1

A possible implementation:

function* drop<T>(iter: Iterable<T>, n: number):
    IterableIterator<T> {
    for (const value of iter) {
        if (n-- > 0) continue;

        yield value;
    }
}

2个

我们可以定义count()一个计数器,它产生从 1 开始的数字并继续运行。就其产生的价值流而言,我们drop()先看前五个,然后take()再看后五个:

函数* count(): IterableIterator<number> {
    让 n: 数字 = 0;

    而(真){
        n++;
        产量 n;
    }
}

for (let value of take(drop(count(), 5), 5)) {
    控制台日志(值);

2

We can define count(), a counter that yields numbers starting from 1 and keeps going. Taking the stream of value it produces, we drop() the first five and then take() the next five:

function* count(): IterableIterator<number> {
    let n: number = 0;

    while (true) {
        n++;
        yield n;
    }
}

for (let value of take(drop(count(), 5), 5)) {
    console.log(value);

 

 

第 10 章。通用算法和迭代器

Chapter 10. Generic algorithms and iterators

本章涵盖

This chapter covers

  • 使用map()filter()reduce()超越数组
  • Using map(), filter(), and reduce()beyond arrays
  • 使用一组通用算法来解决范围广泛的问题
  • Using a set of common algorithms to solve a wide range of problems
  • 确保泛型类型支持所需的契约
  • Ensuring that a generic type supports a required contract
  • 启用具有不同迭代器类别的各种算法
  • Enabling various algorithms with different iterator categories
  • 实现自适应算法
  • Implementing adaptive algorithms

本章都是关于通用算法的——适用于各种数据类型和数据结构的可重用算法。

This chapter is all about generic algorithms—reusable algorithms that work on various data types and data structures.

第 5 章讨论高阶函数时,我们分别查看了map()filter()和的一个版本。这些函数对数组进行操作,但正如我们在前面的章节中看到的那样,迭代器提供了对任何数据结构的良好抽象。我们将从实现这三种与迭代器一起工作的算法的通用版本开始,这样我们就可以将它们应用于二叉树、列表、数组和任何其他可迭代的数据结构。 reduce()

We looked at one version each of map(), filter(), and reduce() in chapter 5, when we discussed higher-order functions. Those functions operated on arrays, but as we saw in the previous chapters, iterators provide a nice abstraction over any data structure. We’ll start by implementing generic versions of these three algorithms that work with iterators, so we can apply them to binary trees, lists, arrays, and any other iterable data structures.

map(), filter(), 和reduce()不是唯一的。我们将讨论可用于大多数现代编程语言的其他通用算法和算法库。我们将看到为什么我们应该用对库算法的调用来替换大多数循环。我们还将简要讨论流畅的 API 以及算法的用户友好界面是什么样的。

map(), filter(), and reduce() are not unique. We’ll talk about other generic algorithms and algorithm libraries that are available to most modern programming languages. We’ll see why we should replace most loops with calls to library algorithms. We’ll also briefly talk about fluent APIs and what a user-friendly interface for algorithms looks like.

接下来,我们将讨论类型参数约束;通用数据结构和算法可以指定它们需要在其参数类型上可用的某些功能。这种类型的专业化允许通用的数据结构和算法,这些数据结构和算法并非在任何地方都适用;它们不太笼统。

Next, we’ll go over type parameter constraints; generic data structures and algorithms can specify certain features they need available on their parameter types. This type of specialization allows for generic data structures and algorithms that don’t work everywhere; they are somewhat less general.

我们将重点关注迭代器并讨论所有不同类别的迭代器。更专业的迭代器支持更高效的算法。权衡是并非所有数据结构都可以支持专门的迭代器。

We’ll zoom in on iterators and talk about all the different categories of iterators. More specialized iterators enable more efficient algorithms. The trade-off is that not all data structures can support specialized iterators.

最后,我们将快速浏览一下自适应算法。此类算法为具有较少功能的迭代器提供更通用、效率较低的实现,并为具有更多功能的迭代器提供更高效、不太通用的实现。

Finally, we’ll take a quick look at adaptive algorithms. Such algorithms provide more general, less efficient implementations for iterators with fewer capabilities and more efficient, less general implementations for iterators with more capabilities.

10.1. 更好的 map()、filter()、reduce()

10.1. Better map(), filter(), reduce()

第 5 章中,我们讨论了map()filter()reduce(),并研究了它们中每一个的可能实现。这些算法是高阶函数,因为它们每个都将另一个函数作为参数并将其应用于序列。

In chapter 5, we talked about map(), filter(), and reduce(), and looked at a possible implementation of each of them. These algorithms are higher-order functions, as they each take another function as an argument and apply it over a sequence.

map()将函数应用于序列的每个元素并返回结果。filter()对每个元素应用过滤函数,并仅返回该函数返回的元素truereduce()使用给定函数组合序列中的所有值,并返回单个值作为结果。

map() applies the function to each element of the sequence and returns the results. filter() applies a filtering function to each element and returns only the elements for which that function returns true. reduce() combines all the values in the sequence, using the given function, and returns a single value as the result.

我们在第 5 章中的实现使用了泛型类型参数T,序列表示为 的数组T

Our implementation in chapter 5 used a generic type parameter T, and the sequences were represented as arrays of T.

10.1.1。地图()

10.1.1. map()

让我们来看看我们是如何实现的map()。我们使用了两个类型参数:TU。该函数将一个T值数组作为第一个参数,并将一个从T到的函数U作为第二个参数。它返回一个值数组U,如下一个清单所示。

Let’s take a look at how we implemented map(). We used two type parameters: T and U. The function takes an array of T values as the first argument and a function from T to U as the second argument. It returns an array of U values, as shown in the next listing.

清单 10.1。map()
function map<T, U>(items: T[], func: (item: T) => U): U[] { 1     let 
    result: U[] = [];                                      2个

    对于(项目的常量项目){
        结果.推送(功能(项目));                               3个
    }

    返回结果;                                             4 
}
function map<T, U>(items: T[], func: (item: T) => U): U[] {    1
    let result: U[] = [];                                      2

    for (const item of items) {
        result.push(func(item));                               3
    }

    return result;                                             4
}

  • 1 map() 接受一个 T 类型的项目数组和一个从 T 到 U 的函数,并返回一个 Us 数组。
  • 1 map() takes an array of items of type T and a function from T to U, and returns an array of Us.
  • 2 从一个空的 Us 数组开始。
  • 2 Start with an empty array of Us.
  • 3 对于每个项目,将 func(item) 的结果推送到 Us 数组。
  • 3 For each item, push the result of func(item) to the array of Us.
  • 4 返回我们的数组。
  • 4 Return the array of Us.

现在我们了解了迭代器和生成器,让我们在下一个清单中看看我们如何实现以map()在 any 上工作Iterable<T>,而不仅仅是数组。

Now that we know about iterators and generators, let’s see in the next listing how we can implement map() to work on any Iterable<T>, not only arrays.

清单 10.2。map()带迭代器
function * map<T, U>( iter: Iterable<T> , func: (item: T) => U):     1
     IterableIterator<U> {                                         2
    for (iter 的常量值) {
        收益函数(值);                                       3个
    }
}
function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):    1
    IterableIterator<U> {                                        2
    for (const value of iter) {
        yield func(value);                                       3
    }
}

  • 1 map() 现在是一个将 Iterable<T> 作为第一个参数的生成器。
  • 1 map() is now a generator that takes an Iterable<T> as the first argument.
  • 2 map() 返回一个 IterableIterator<U>。
  • 2 map() returns an IterableIterator<U>.
  • 3 给定函数应用于从迭代器检索的每个值,并产生结果。
  • 3 The given function is applied to each value retrieved from the iterator, and the result is yielded.

虽然最初的实现仅限于数组,但这个实现适用于提供迭代器的任何数据结构。不仅如此,它还更加简洁。

Whereas the original implementation was restricted to arrays, this one works with any data structure that provides an iterator. Not only that, but it is also more concise.

10.1.2。筛选()

10.1.2. filter()

让我们对 做同样的事情filter()。我们最初的实现需要一个类型数组T和一个谓词。提醒一下,谓词是一种函数,它接受一个某种类型的参数并返回一个boolean. 如果函数返回true该值,我们说该值满足谓词。

Let’s do the same for filter(). Our original implementation expected an array of type T and a predicate. As a reminder, a predicate is a function that takes one argument of some type and returns a boolean. We say that a value satisfies the predicate if the function returns true for that value.

清单 10.3。filter()
function filter<T>(items: T[], pred: (item: T) => boolean): T[] {    1
    让结果:T[] = [];

    对于(项目的常量项目){
        如果(预测(项目)){                                            2
            结果.推送(项目);
        }
    }

    返回结果;
}
function filter<T>(items: T[], pred: (item: T) => boolean): T[] {   1
    let result: T[] = [];

    for (const item of items) {
        if (pred(item)) {                                           2
            result.push(item);
        }
    }

    return result;
}

  • 1 filter() 采用 T 数组和谓词(从 T 到布尔值的函数)。
  • 1 filter() takes an array of Ts and a predicate (a function from T to boolean).
  • 2 如果谓词返回真,该项被添加到结果数组;否则,它被跳过。
  • 2 If the predicate returns true, the item is added to the result array; otherwise, it’s skipped.

正如我们对 map() 所做的那样,我们将使用 anIterable<T>而不是数组并将此可迭代对象实现为生成器,该生成器会生成满足谓词的值,如以下清单所示。

Just as we did with map(), we are going to use an Iterable<T> instead of an array and implement this iterable as a generator that yields values that satisfy the predicate, as shown in the following listing.

清单 10.4。filter()带迭代器
函数* filter<T>(iter: Iterable<T> , pred: (item: T) => boolean):     1
     IterableIterator<T> {                                               2
    for (iter 的常量值) {
        如果(预测值(值)){

            屈服值;                                               3个
        }
    }
}
function* filter<T>(iter: Iterable<T>, pred: (item: T) => boolean):    1
    IterableIterator<T> {                                              2
    for (const value of iter) {
        if (pred(value)) {

            yield value;                                               3
        }
    }
}

  • 1 filter() 现在是一个将 Iterable<T> 作为第一个参数的生成器。
  • 1 filter() is now a generator that takes an Iterable<T> as the first argument.
  • 2 filter() 返回一个 IterableIterator<T>。
  • 2 filter() returns an IterableIterator<T>.
  • 3 如果一个值满足谓词,则产生它。
  • 3 If a value satisfies the predicate, it is yielded.

我们再次以一个更短的实现结束,它不仅可以处理数组。最后,让我们更新一下reduce()

We again end up with a shorter implementation that works with more than arrays. Finally, let’s update reduce().

10.1.3。减少()

10.1.3. reduce()

我们最初的实现reduce()期望一个 array of T,一个 type 的初始值T(如果数组为空)和一个 operation op()。该操作是一个函数,它接受两个类型的值T并返回一个类型的值Treduce()将操作应用于初始值和数组的第一个元素,存储结果,将操作应用于结果和数组的下一个元素,依此类推。

Our original implementation of reduce() expected an array of T, an initial value of type T (in case the array is empty), and an operation op(). The operation is a function that takes two values of type T and returns a value of type T. reduce() applies the operation to the initial value and the first element of the array, stores the result, applies the operation to the result and the next element of the array, and so on.

清单 10.5。reduce()
function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T {    1
    让结果:T = init;

    对于(项目的常量项目){
        结果 = 操作(结果,项目);                                    2个
    }

    返回结果;
}
function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T {   1
    let result: T = init;

    for (const item of items) {
        result = op(result, item);                                    2
    }

    return result;
}

  • 1 reduce() 采用 T 数组、初始值和将两个 T 合二为一的操作。
  • 1 reduce() takes an array of Ts, an initial value, and an operation combining two Ts into one.
  • 2 使用给定的操作,数组中的每一项都与运行总计相结合。
  • 2 Each item in the array is combined with the running total, using the given operation.

我们可以重写它以使用一个Iterable<T>instead ,以便它适用于任何序列,如以下代码所示。在这种情况下,我们不需要生成器。与前面两个函数不同,reduce()返回的不是元素序列,而是单个值。

We can rewrite this to use an Iterable<T> instead so that it works with any sequence, as shown in the following code. In this case, we don’t need a generator. Unlike the previous two functions, reduce() does not return a sequence of elements, but a single value.

清单 10.6。reduce()带迭代器
函数 reduce<T>( iter: Iterable<T> , init: T,       1
    op: (x: T, y: T) => T): T {
    让结果:T = init;

    for (iter 的常量值) {
        结果=操作(结果,价值);
    }

    返回结果;
}
function reduce<T>(iter: Iterable<T>, init: T,      1
    op: (x: T, y: T) => T): T {
    let result: T = init;

    for (const value of iter) {
        result = op(result, value);
    }

    return result;
}

  • 1 reduce() 不是 T 数组,而是将 Iterable<T> 作为其第一个参数。
  • 1 Instead of an array of T, reduce() takes an Iterable<T> as its first argument.

其余的实现没有改变。

The rest of the implementation is unchanged.

10.1.4。过滤器()/减少()管道

10.1.4. filter()/reduce() pipeline

让我们看看我们如何将这些算法组合成一个管道,该管道仅从二叉树中获取偶数并将它们相加。我们将使用第 9 章BinaryTreeNode<T>中的顺序遍历,并将其与偶数过滤器和使用加法链接起来作为操作。 reduce()

Let’s see how we can combine these algorithms into a pipeline that takes only the even numbers from a binary tree and sums them up. We’ll use our BinaryTreeNode<T> from chapter 9, with its in-order traversal, and chain this with an even number filter and a reduce() using addition as the operation.

清单 10.7。filter()/reduce()管道
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);    1个
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

常量结果:数字 =
    减少(
        筛选(
            inOrderIterator(root),                            2 
            (value) => value % 2 == 0),                       3 
        0, (x, y) => x + y);                                 4个

控制台日志(结果);
let root: BinaryTreeNode<number> = new BinaryTreeNode(1);    1
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

const result: number =
    reduce(
        filter(
            inOrderIterator(root),                           2
            (value) => value % 2 == 0),                      3
        0, (x, y) => x + y);                                 4

console.log(result);

  • 1 我们在上一章中使用的相同示例二叉树
  • 1 The same example binary tree we used in the previous chapter
  • 2 我们得到一个按顺序遍历树的 IterableIterator<number>。
  • 2 We get an IterableIterator<number> that traverses the tree in order.
  • 3 我们使用仅当数字为偶数时才返回 true 的 lambda 进行过滤。
  • 3 We filter using a lambda that returns true only if a number is even.
  • 4 我们使用对两个数字求和的 lambda 从初始值 0 开始减少。
  • 4 We reduce from an initial value of 0 with a lambda that sums two numbers.

这个例子应该加强泛型的强大程度。我们不必实现一个新的功能来遍历二叉树并求和偶数,我们只是简单地组装了一个为这个场景定制的处理管道。

This example should reinforce how powerful generics are. Instead of having to implement a new function to traverse the binary tree and sum up even numbers, we simply put together a processing pipeline customized for this scenario.

10.1.5。练习

10.1.5. Exercises

1个

通过连接所有非空字符串来构建一个处理可迭代类型的管道string

1

Build a pipeline that processes an iterable of type string by concatenating all nonempty strings.

2个

number通过选择所有奇数并对它们进行平方来 构建一个处理可迭代类型的管道。

2

Build a pipeline that processes an iterable of type number by selecting all odd numbers and squaring them.

10.2. 常用算法

10.2. Common algorithms

我们在第 9 章中查看了map()filter()reduce(),并且也提到了。许多其他算法通常用于流水线。让我们列出其中的一些。我们不会查看实现——只描述除了他们期望的可迭代对象之外还有哪些参数以及他们如何处理数据。我们还将提到算法可能出现的一些同义词: take()

We looked at map(), filter(), and reduce(), and also mentioned take() in chapter 9. Many other algorithms are commonly used in pipelines. Let’s list a few of them. We will not look at the implementations—just describe what arguments besides the iterable they expect and how they process the data. We’ll also mention some synonyms under which the algorithm might appear:

  • map()接受一个值序列T和一个函数(value: T) => U,并返回一个值序列U,将函数应用于序列中的所有元素。它也被称为fmap()select()
  • map() takes a sequence of T values and a function (value: T) => U, and returns a sequence of U values, applying the function to all the elements in the sequence. It is also known as fmap(), select().
  • filter()接受一个值序列T和一个谓词(value: T) => boolean,并返回一个值序列T,其中包含谓词返回的所有项目true。它也被称为where().
  • filter() takes a sequence of T values and a predicate (value: T) => boolean, and returns a sequence of T values containing all the items for which the predicate returns true. It is also known as where().
  • reduce()采用一系列T值、初始值 typeT和将两个T值合并为一个的操作(x: T, y: T) => TT它在使用操作组合序列中的所有元素后返回单个值。它也被称为fold(), collect(), accumulate(), aggregate()
  • reduce()takes a sequence of T values, an initial value of type T, and an operation that combines two T values into one (x: T, y: T) => T. It returns a single value T after combining all the elements in the sequence using the operation. It is also known as fold(), collect(), accumulate(), aggregate().
  • any()接受一个值序列T和一个谓词(value: T) => booleantrue如果序列中的任何一个元素满足谓词, 它就会返回。
  • any()takes a sequence of T values and a predicate (value: T) => boolean. It returns true if any one of the elements of the sequence satisfies the predicate.
  • all()接受一个值序列T和一个谓词(value: T) => booleantrue如果序列的所有元素都满足谓词, 则返回。
  • all()takes a sequence of T values and a predicate (value: T) => boolean. It returns true if all the elements of the sequence satisfy the predicate.
  • none()接受一个值序列T和一个谓词(value: T) => booleantrue如果序列中没有元素满足谓词, 则返回。
  • none()takes a sequence of T values and a predicate (value: T) => boolean. It returns true if none of the elements of the sequence satisfies the predicate.
  • take()接受一个值序列T和一个数字 n。n它返回由原始序列的第一个元素组成的序列。它也被称为limit().
  • take() takes a sequence of T values and a number n. It returns a sequence consisting of the first n elements of the original sequence. It is also known as limit().
  • drop()接受一个值序列T和一个数字 n。它返回一个序列,该序列由原始序列中除第一个 n 之外的所有元素组成。丢弃前 n 个元素。它也被称为skip().
  • drop() takes a sequence of T values and a number n. It returns a sequence consisting of all the elements of the original sequence except the first n. The first n elements are dropped. It is also known as skip().
  • zip()接受一个值序列T和一个值序列U。它返回一个包含成对的TU值的序列,有效地将两个序列压缩在一起。
  • zip() takes a sequence of T values and a sequence of U values. It returns a sequence containing pairs of T and U values, effectively zipping together the two sequences.

还有更多用于排序、反转、拆分和连接序列的算法。好消息是,由于这些算法非常有用且普遍适用,我们不需要实施它们。大多数语言都有提供这些算法和更多算法的库。JavaScript 有underscore.jspackage 和lodashpackage,它们都提供了大量这样的算法。(在撰写本文时,这些库不支持迭代器——仅支持 JavaScript 内置数组和对象类型。)在 Java 中,它们位于包中java.util.stream。在 C# 中,它们位于System.Linq命名空间中。在 C++ 中,它们位于<algorithm>标准库头文件中。

There are many more algorithms for sorting, reversing, splitting, and concatenating sequences. The good news is that because these algorithms are so useful and generally applicable, we don’t need to implement them. Most languages have libraries that provide these algorithms and more. JavaScript has the underscore.js package and the lodash package, both of which provide a plethora of such algorithms. (At the time of writing, these libraries don’t support iterators—only the JavaScript built-in array and object types.) In Java, they are in the java.util.stream package. In C#, they are in the System.Linq namespace. In C++, they are in the <algorithm> standard library header.

10.2.1。算法而不是循环

10.2.1. Algorithms instead of loops

您可能会感到惊讶,一个好的经验法则是在您发现自己编写循环时检查库算法或管道是否可以完成这项工作。通常,我们编写循环来处理一个序列,这正是我们讨论的算法所做的。

You may be surprised that a good rule of thumb is to check, whenever you find yourself writing a loop, whether a library algorithm or a pipeline can do the job. Usually, we write loops to process a sequence, which is exactly what the algorithms we talked about do.

在循环中更喜欢库算法而不是自定义代码的原因是出错的机会更少。库算法经过反复试验和有效实施,我们最终得到的代码更容易理解,因为操作被拼写出来了。

The reason to prefer library algorithms to custom code in loops is that there is less opportunity for mistakes. Library algorithms are tried and tested and implemented efficiently, and the code we end up with is easier to understand, as the operations are spelled out.

我们在本书中研究了一些实现,以更好地理解事物的底层工作原理,但您很少需要自己实现算法。如果您最终遇到可用算法无法解决的问题,请考虑对您的解决方案进行通用的、可重用的实现,而不是一次性的特定实现。

We’ve looked at a few implementations throughout this book to get a better understanding of how things work under the hood, but you’ll rarely need to implement an algorithm yourself. If you do end up with a problem that the available algorithms can’t solve, consider making a generic, reusable implementation of your solution rather than a one-off specific implementation.

10.2.2。实施流畅的管道

10.2.2. Implementing a fluent pipeline

大多数库还提供流畅的 API 以将算法链接到管道中。Fluent API 是基于方法链的 API,使代码更易于阅读。要了解流畅 API 和非流畅 API 之间的区别,让我们再看一下 10.1.4 节中的过滤/归约管道。

Most libraries also provide a fluent API to chain algorithms into a pipeline. Fluent APIs are APIs based on method chaining, making the code much easier to read. To see the difference between a fluent and a nonfluent API, let’s take another look at the filter/reduce pipeline from section 10.1.4.

清单 10.8。过滤/减少管道
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

常量结果:数字 =
    减少(
        筛选(
            inOrderBinaryTreeIterator(root),
            (值) => 值 % 2 == 0),
        0, (x, y) => x + y);

控制台日志(结果);
let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

const result: number =
    reduce(
        filter(
            inOrderBinaryTreeIterator(root),
            (value) => value % 2 == 0),
        0, (x, y) => x + y);

console.log(result);

即使我们filter()先应用然后将结果传递给reduce(),但如果我们从左到右阅读代码,我们会看到reduce()之前filter()。也很难理解哪些参数与管道中的哪个函数一起使用。流畅的 API 使代码更易于阅读。

Even though we apply filter() first and then pass the result to reduce(), if we read the code from left to right, we see reduce() before filter(). It’s also a bit hard to make sense of which arguments go with which function in the pipeline. Fluent APIs make the code much easier to read.

目前,我们所有的算法都将可迭代对象作为第一个参数并返回一个迭代器。我们可以使用面向对象编程来改进我们的 API。我们可以将所有算法放在一个包装可迭代对象的类中。然后我们可以调用任何可迭代对象而无需显式提供可迭代对象作为第一个参数;可迭代对象是一个成员类的。让我们为map(),执行此操作filter(),并将reduce()它们分组到一个包装可迭代对象的新FluentIterable<T>类中,如下一个清单所示。

Currently, all our algorithms take an iterable as the first argument and return an iterator. We can use object-oriented programming to improve our API. We can put all our algorithms in a class that wraps an iterable. Then we can call any of the iterables without explicitly providing an iterable as the first argument; the iterable is a member of the class. Let’s do this for map(), filter(), and reduce() by grouping them into a new FluentIterable<T> class wrapping an iterable, as shown in the next listing.

清单 10.9。流利的可迭代
类 FluentIterable<T> {                                         1 
    iter: Iterable<T>;                                           1个

    构造函数(iter: Iterable<T>) {
        这个.iter = iter;
    }

    *map<U>(func: (item: T) => U): IterableIterator<U> {          2
        for (this.iter 的常量值) {
            收益函数(值);
        }
    }

    *filter(pred: (item: T) => boolean): IterableIterator<T> {    2
        for (this.iter 的常量值) {
            如果(预测值(值)){
                屈服值;
            }
        }
    }

    减少(初始化:T,操作:(x:T,y:T)=> T):T {                   2
        让结果:T = init;

        for (this.iter 的常量值) {
            结果=操作(结果,价值);
        }

        返回结果;
    }
}
class FluentIterable<T> {                                        1
    iter: Iterable<T>;                                           1

    constructor(iter: Iterable<T>) {
        this.iter = iter;
    }

    *map<U>(func: (item: T) => U): IterableIterator<U> {         2
        for (const value of this.iter) {
            yield func(value);
        }
    }

    *filter(pred: (item: T) => boolean): IterableIterator<T> {   2
        for (const value of this.iter) {
            if (pred(value)) {
                yield value;
            }
        }
    }

    reduce(init: T, op: (x: T, y: T) => T): T {                  2
        let result: T = init;

        for (const value of this.iter) {
            result = op(result, value);
        }

        return result;
    }
}

  • 1 FluentIterable<T> 包装了一个 Iterable<T>。
  • 1 FluentIterable<T> wraps an Iterable<T>.
  • 2 map()、filter() 和 reduce() 与之前的实现类似,但它们不是将可迭代对象作为第一个参数,而是使用 this.iter 可迭代对象。
  • 2 map(), filter(), and reduce() are similar to the previous implementations, but instead of taking an iterable as the first argument, they use the this.iter iterable.

FluentIterable<T>我们可以从 中创建一个Iterable<T>,这样我们就可以将filter()/reduce()管道重写为更流畅的形式。我们创建一个Fluent-Iterable<T>,调用它,从它的结果filter()创建一个新的,然后调用它,如以下清单所示。 FluentIterable<T>reduce()

We can create a FluentIterable<T> out of an Iterable<T>, so we can rewrite our filter()/reduce() pipeline into a more fluent form. We create a Fluent-Iterable<T>, call filter() on it, create a new FluentIterable<T> from its result, and call reduce() on it, as the following listing shows.

清单 10.10。流畅的过滤器/减少管道
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

常量结果:数字 =
    新的 FluentIterable(
        新的 FluentIterable(
            inOrderIterator(root)                1 
        ).filter((value) => value % 2 == 0)      2 
    ).reduce(0, (x, y) => x + y);               3个

控制台日志(结果);
let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

const result: number =
    new FluentIterable(
        new FluentIterable(
            inOrderIterator(root)               1
        ).filter((value) => value % 2 == 0)     2
    ).reduce(0, (x, y) => x + y);               3

console.log(result);

  • 1 我们从 inOrderIterator 获取二叉树上的可迭代对象,并使用它来初始化 FluentIterable。
  • 1 We get an iterable over the binary tree from inOrderIterator and use it to initialize a FluentIterable.
  • 2 我们在 FluentIterable 上调用 filter(),然后根据结果创建另一个 FluentIterable。
  • 2 We call filter() on the FluentIterable and then create another FluentIterable from the result.
  • 3 最后,我们在 FluentIterable 上调用 reduce() 以获得最终结果。
  • 3 Finally, we call reduce() on the FluentIterable to get the final result.

Nowfilter()出现在 之前reduce(),很明显参数转到了那个函数。唯一的问题是我们需要在每次函数调用后创建一个新的Fluent-Iterable<T>。我们可以通过让我们的map()filter()函数返回 aFluentIterable<T>而不是默认值来改进我们的 API IterableIterator<T>。请注意,我们不需要更改reduce(),因为reduce()返回类型的单个值T,而不是可迭代的。

Now filter() appears before reduce(), and it’s very clear that arguments go to that function. The only problem is that we need to create a new Fluent-Iterable<T> after each function call. We can improve our API by having our map() and filter() functions return a FluentIterable<T> instead of the default IterableIterator<T>. Note that we don’t need to change reduce(), because reduce() returns a single value of type T, not an iterable.

因为我们正在使用生成器,所以我们不能简单地更改返回类型。生成器的存在是为了为函数提供方便的语法,但它们总是返回一个IterableIterator<T>. 相反,我们可以将实现转移到几个私有方法——mapImpl()和——并处理公共方法和方法中从到的filterImpl()转换,如以下清单所示。 IterableIterator<T>FluentIterable<T>map()reduce()

Because we’re using generators, we can’t simply change the return type. Generators exist to provide convenient syntax for functions, but they always return an IterableIterator<T>. Instead, we can move the implementations to a couple of private methods—mapImpl() and filterImpl()—and handle the conversion from IterableIterator<T> to FluentIterable<T> in the public map() and reduce() methods, as shown in the following listing.

清单 10.11。更流畅的迭代
类 FluentIterable<T> {
    iter: 可迭代<T>;

    构造函数(iter: Iterable<T>) {
        这个.iter = iter;
    }

    map<U>(func: (item: T) => U): FluentIterable<U> {
        返回新的 FluentIterable(this.mapImpl(func));                    1个
    }

    private *mapImpl<U>(func: (item: T) => U): IterableIterator<U> {
        for (this.iter 的常量值) {                                   2
            收益函数(值);
        }
    }

    filter<U>(pred: (item: T) => boolean): FluentIterable<T> {
        返回新的 FluentIterable(this.filterImpl(pred));                 3个
    }

    private *filterImpl(pred: (item: T) => boolean): IterableIterator<T> {
        for (this.iter 的常量值) {                                   4
            如果(预测值(值)){
                屈服值;
            }
        }
    }

    减少(初始化:T,操作:(x:T,y:T)=> T):T {                            5
        让结果:T = init;

        for (this.iter 的常量值) {
            结果=操作(结果,价值);
        }

        返回结果;
    }
}
class FluentIterable<T> {
    iter: Iterable<T>;

    constructor(iter: Iterable<T>) {
        this.iter = iter;
    }

    map<U>(func: (item: T) => U): FluentIterable<U> {
        return new FluentIterable(this.mapImpl(func));                    1
    }

    private *mapImpl<U>(func: (item: T) => U): IterableIterator<U> {
        for (const value of this.iter) {                                  2
            yield func(value);
        }
    }

    filter<U>(pred: (item: T) => boolean): FluentIterable<T> {
        return new FluentIterable(this.filterImpl(pred));                 3
    }

    private *filterImpl(pred: (item: T) => boolean): IterableIterator<T> {
        for (const value of this.iter) {                                  4
            if (pred(value)) {
                yield value;
            }
        }
    }

    reduce(init: T, op: (x: T, y: T) => T): T {                           5
        let result: T = init;

        for (const value of this.iter) {
            result = op(result, value);
        }

        return result;
    }
}

  • 1 map() 将其参数转发给 mapImpl() 并将结果转换为 FluentIterable。
  • 1 map() forwards its argument to mapImpl() and converts the result to a FluentIterable.
  • 2 mapImpl() 是带有生成器的原始 map() 实现。
  • 2 mapImpl() is the original map() implementation with a generator.
  • 3 与 map() 一样,filter() 将其参数转发给 filterImpl() 并将结果转换为 FluentIterable。
  • 3 Like map(), filter() forwards its argument to filterImpl() and converts the result to a FluentIterable.
  • 4 filterImpl() 是带有生成器的原始 filter() 实现。
  • 4 filterImpl() is the original filter() implementation with a generator.
  • 5 reduce() 保持不变,因为它不返回迭代器。
  • 5 reduce() stays unchanged because it doesn’t return an iterator.

通过这个更新的实现,我们可以更轻松地链接算法,因为每个算法都返回一个FluentIterable包含所有算法的方法,如下一个清单所示。

With this updated implementation, we can more easily chain the algorithms, as each returns a FluentIterable that contains all the algorithms as methods, as shown in the next listing.

清单 10.12。更流畅的过滤器/减少管道
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

常量结果:数字 =
    new FluentIterable(inOrderIterator(root))       1 
    .filter((value) => value % 2 == 0)              2 
    .reduce(0, (x, y) => x + y);                   3个

控制台日志(结果);
let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);

const result: number =
    new FluentIterable(inOrderIterator(root))      1
    .filter((value) => value % 2 == 0)             2
    .reduce(0, (x, y) => x + y);                   3

console.log(result);

  • 1 我们只需要从树上的原始迭代器明确地新建一个 FluentIterable 一次。
  • 1 We need to explicitly new up a FluentIterable only once, from the original iterator over the tree.
  • 2 filter()是FluentIterable的一个方法,返回一个FluentIterable本身。
  • 2 filter() is a method of FluentIterable and returns a FluentIterable itself.
  • 3 我们可以对 filter() 的结果调用 reduce()。
  • 3 We can call reduce() on the result of filter().

现在,以真正流畅的方式,代码很容易从左到右阅读,我们可以使用非常自然的语法链接构成我们管道的任意数量的算法。大多数算法库都采用类似的方法,从而尽可能轻松地链接多个算法。

Now, in true fluent fashion, the code reads easily from left to right, and we can chain any number of algorithms that make up our pipeline with a very natural syntax. Most algorithm libraries take a similar approach, making it as easy as possible to chain multiple algorithms.

根据编程语言的不同,流畅的 API 方法的一个缺点是我们FluentIterable最终会包含所有算法,因此它是难以延伸。如果它是库的一部分,则调用代码在不修改类的情况下无法轻松添加新算法。C# 提供扩展方法,使我们能够在不修改其代码的情况下向类或接口添加方法。不过,并非所有语言都具有此类功能。也就是说,在大多数情况下,我们应该使用现有的算法库,而不是从头开始实现一个新的算法库。

Depending on the programming language, one downside of a fluent API approach is that our FluentIterable ends up containing all the algorithms, so it is difficult to extend. If it is part of a library, calling code can’t easily add a new algorithm without modifying the class. C# provides extension methods, which enable us to add methods to a class or interface without modifying its code. Not all languages have such features, though. That being said, in most situations, we should be using an existing algorithm library, not implementing a new one from scratch.

10.2.3。练习

10.2.3. Exercises

1个

扩展FluentIterablewith take(),从迭代器返回前 n 个元素的算法。

1

Extend FluentIterable with take(), the algorithm that returns the first n elements from an iterator.

2个

扩展FluentIterablewith drop(),该算法跳过迭代器的前 n 个元素并返回其余元素。

2

Extend FluentIterable with drop(), the algorithm that skips the first n elements of an iterator and returns the rest.

10.3. 约束类型参数

10.3. Constraining type parameters

我们看到了通用数据结构如何赋予数据形状,而不管其特定类型参数T是什么。我们还研究了一组算法,这些算法使用迭代器来处理某种类型的值序列T,而不管该类型是什么。现在让我们看看下面清单中的一个场景,其中类型很重要:我们有一个renderAll()通用函数,它接受一个as 参数并在迭代器的每个元素上 Iterable<T>调用该方法。render()

We saw how a generic data structure gives shape to the data, regardless of what its specific type parameter T is. We also looked at a set of algorithms that uses iterators to process sequences of values of some type T, regardless of what that type is. Now let’s look at a scenario in the following listing in which the type matters: we have a renderAll() generic function that takes an Iterable<T> as argument and calls the render() method on each element of the iterator.

清单 10.13。renderAll草图
函数 renderAll<T>(iter: Iterable<T>): void {     1
    for (iter 的常量项) {
        item.render();                              2个
    }
}.
function renderAll<T>(iter: Iterable<T>): void {    1
    for (const item of iter) {
        item.render();                              2
    }
}.

  • 1 renderAll() 将 Iterable<T> 作为参数。
  • 1 renderAll() takes an Iterable<T> as argument.
  • 2 我们对迭代器返回的每个项目调用 render()。
  • 2 We call render() on each item returned by the iterator.

函数编译失败,错误信息如下:

The function fails to compile, with the following error message:

类型“T”上不存在属性“render”。
Property 'render' does not exist on type 'T'.

我们正在尝试调用render()泛型类型T,但我们不能保证该类型上存在这样的方法。对于这种类型的场景,我们需要一种方法来约束类型T,以便它只能被具有render()方法的类型实例化。

We are attempting to call render() on a generic type T, but we have no guarantee that such a method exists on the type. For this type of scenario, we need a way to constrain the type T so that it can be instantiated only with types that have a render() method.

类型参数的约束

约束告知编译器类型参数必须具备的能力。没有任何约束,类型参数可以是任何类型。一旦我们要求某些成员在泛型类型上可用,我们就会使用约束将允许的类型集限制为具有所需成员的类型。

Constraints inform the compiler about the capabilities that a type argument must have. Without any constraints, the type argument could be any type. As soon as we require certain members to be available on a generic type, we use constraints to restrict the set of allowed types to those that have the required members.

在我们的例子中,我们可以定义一个IRenderable声明方法的接口render(),如下一个清单所示。T然后我们可以通过使用extends关键字告诉编译器我们只接受类型参数来添加约束IRenderable

In our case, we can define an IRenderable interface that declares a render() method, as shown in the next listing. Then we can add a constraint on T by using the extends keyword to tell the compiler that we accept only type arguments that are IRenderable.

清单 10.14。renderAll有限制
接口 IRenderable {                                                 1
    渲染():无效;
}

函数 renderAll<T extends IRenderable >(iter: Iterable<T>): void {    2
    for (iter 的常量项) {
        item.render();
    }
}
interface IRenderable {                                                1
    render(): void;
}

function renderAll<T extends IRenderable>(iter: Iterable<T>): void {   2
    for (const item of iter) {
        item.render();
    }
}

  • 1 IRenderable 接口要求实现者提供 render() 方法。
  • 1 IRenderable interface requires implementers to provide a render() method.
  • 2 T 扩展 IRenderable 并告诉编译器只接受将 IRenderable 实现为 T 的类型。
  • 2 T extends IRenderable and tells the compiler to accept only types that implement IRenderable as T.

10.3.1。具有类型约束的通用数据结构

10.3.1. Generic data structures with type constraints

大多数通用数据结构不需要限制它们的类型参数。我们可以将任何类型的值存储在链表、树或数组中。不过也有一些例外,例如哈希集。

Most generic data structures don’t need to constrain their type parameters. We can store values of any type in a linked list, a tree, or an array. There are a few exceptions, though, such as a hash set.

集合数据结构模拟数学集合,因此它存储唯一值,丢弃重复值。集合数据结构通常提供合并、相交和减去其他集合的方法。它们还提供了一种方法来检查给定值是否已经是集合的一部分。要检查一个值是否已经是集合的一部分,我们可以将它与集合中的每个元素进行比较,但这种方法并不是最有效的。与集合中的每个元素进行比较需要我们在最坏的情况下遍历整个集合。这样的遍历需要线性时间,或 O(n)。请参阅下一页的边栏“大 O 表示法”以进行复习。

A set data structure models a mathematical set, so it stores unique values, discarding duplicates. Set data structures usually provide methods to union, intersect, and subtract other sets. They also provide a way to check whether a given value is already part of the set. To check whether a value is already part of a set, we can compare it with every element of the set, but that approach is not the most efficient. Comparing with every element in the set requires us, in the worst case, to traverse the whole set. Such a traversal requires linear time, or O(n). See the sidebar “Big O notation” on the next page for a refresher.

更高效的实现可以散列每个值并将其存储在键值数据结构中,如散列映射或字典。这样的数据结构可以在常数时间或 O(1)中检索值,从而使它们更高效。散列集包装了散列映射,可以提供高效的成员资格检查。但它确实有一个约束:类型T需要提供一个哈希函数,它接受一个类型的值T并返回一个number:它的哈希值。

A more efficient implementation can hash each value and store it in a key-value data structure like a hash map or dictionary. Such data structures can retrieve a value in constant time, or O(1), making them more efficient. A hash set wraps a hash map and can provide efficient membership checks. But it does come with a constraint: the type T needs to provide a hash function, which takes a value of type T and returns a number: its hash value.

某些语言通过在其顶级类型上提供散列方法来确保所有值都可以散列。Java 顶级类型Object有一个hashCode()方法,而 C#Object顶级类型有一个GetHashCode()方法。但是如果一种语言没有,我们需要一个类型约束来确保只有可散列的类型可以存储在数据结构中。IHashable例如, 我们可以定义一个接口,并使其成为我们通用哈希映射或字典的键类型的类型约束。

Some languages ensure that all values can be hashed by providing a hash method on their top type. The Java top type, Object, has a hashCode() method, whereas the C# Object top type has a GetHashCode() method. But if a language doesn’t have that, we need a type constraint to ensure that only hash-able types can be stored in the data structures. We could define an IHashable interface, for example, and make it a type constraint on the key type of our generic hash map or dictionary.

大 O 表示法

大 O 表示法为函数执行所需的时间和空间提供了上限,因为它的参数趋向于特定值 n。我们不会深入探讨这个话题;相反,我们将概述一些常见的上限并解释它们的含义。

Big O notation provides an upper bound to the time and space required by a function to execute as its arguments tend toward a particular value n. We won’t go too deep into this topic; instead we’ll outline a few common upper bounds and explain what they mean.

Constant time或 O(1),意味着函数的执行时间不取决于它必须处理的项目数。first()例如,获取序列第一个元素的 函数对于 2 或 200 万个项目的序列运行速度一样快。

Constant time, or O(1), means that a function’s execution time does not depend on the number of items it has to process. The function first(), which takes the first element of a sequence, runs just as fast for a sequence of 2 or 2 million items, for example.

对数时间,或 O(log n),意味着该函数每一步将其输入减半,因此即使对于较大的 n 值,它也非常有效。一个例子是在排序序列中进行二分查找。

Logarithmic time, or O(log n), means that the function halves its input with each step, so it is very efficient even for large values of n. An example is binary search in a sorted sequence.

线性时间,或 O(n),意味着函数运行时间与其输入成比例增长。遍历一个序列是O(n),比如判断一个序列的所有元素是否满足某个谓词。

Linear time, or O(n), means that the function run time grows proportionally with its input. Looping over a sequence is O(n), such as determining whether all elements of a sequence satisfy some predicate.

二次时间或 O(n 2 ) 的效率远低于线性时间,因为运行时间的增长速度远快于输入大小的增长速度。序列上的两个嵌套循环的运行时间为 O(n 2 )。

Quadratic time, or O(n2), is much less efficient than linear, as the run time grows much faster than the size of the input. Two nested loops over a sequence have a run time of O(n2).

Linearithmic或 O(n log n) 不如线性有效,但比二次有效。最有效的比较排序算法是 O(n log n);我们无法用单个循环对序列进行排序,但我们可以比两个嵌套循环更快地完成排序。

Linearithmic, or O(n log n), is not as efficient as linear but more efficient than quadratic. The most efficient comparison sort algorithms are O(n log n); we can’t sort a sequence with a single loop, but we can do it faster than two nested loops.

正如时间复杂度设置了一个函数的运行时间如何随着其输入大小的增加而增加的上限一样,空间复杂度设置了一个函数随着其输入大小的增长而需要的额外内存量的上限。

Just as time complexity sets an upper bound on how the run time of a function increases with the size of its input, space complexity sets an upper bound on the amount of additional memory a function needs as the size of its input grows.

常量空间,或 O(1),意味着随着输入大小的增长,函数不需要更多空间。max()例如,我们的函数需要一些额外的内存来存储运行最大值和迭代器,但无论序列有多大,内存量都是恒定的 。

Constant space, or O(1), means that a function doesn’t need more space as the size of the input grows. Our max() function, for example, requires some extra memory to store the running maximum and the iterator, but the amount of memory is constant regardless of how large the sequence is.

线性空间,或 O(n),意味着函数需要的内存量与其输入的大小成正比。这种函数的一个例子是我们最初的in-Order()二叉树遍历,它将所有节点的值复制到一个数组中以提供树上的迭代器。

Linear space, or O(n), means that the amount of memory a function needs is proportional to the size of its input. An example of such a function is our original in-Order() binary tree traversal, which copied the values of all nodes into an array to provide an iterator over the tree.

10.3.2。具有类型约束的通用算法

10.3.2. Generic algorithms with type constraints

算法往往比数据结构对其类型有更多的限制。如果我们想要对一组值进行排序,我们需要一种方法来比较这些值。同样,如果我们要确定一个序列的最小或最大元素,则该序列的元素需要具有可比性。

Algorithms tend to have more constraints on their types than data structures. If we want to sort a set of values, we need a way to compare those values. Similarly, if we want to determine the minimum or maximum element of a sequence, the elements of that sequence need to be comparable.

让我们看看max()下一个清单中通用算法的可能实现。首先,我们将声明一个IComparable<T>接口并约束我们的算法使用它。该接口声明了一个compareTo()方法。

Let’s look at a possible implementation of a max() generic algorithm in the next listing. First, we will declare an IComparable<T> interface and constrain our algorithm to use it. The interface declares a single compareTo() method.

清单 10.15。IComparable界面
枚举比较结果 {                        1
    少于,
    平等的,
    比...更棒
}

接口 IComparable<T> {
    compareTo(value: T): 比较结果;    2 
}
enum ComparisonResult {                       1
    LessThan,
    Equal,
    GreaterThan
}

interface IComparable<T> {
    compareTo(value: T): ComparisonResult;    2
}

  • 1 ComparisonResult 表示比较的结果。
  • 1 ComparisonResult represents the result of a comparison.
  • 2 IComparable 声明了一个 compareTo 接口,它将当前实例与另一个相同类型的值进行比较,并返回一个 Comparison 结果。
  • 2 IComparable declares a compareTo interface that compares the current instance with another value of the same type and returns a Comparison result.

现在让我们实现一个max()通用算法,该算法期望对一IComparable组值进行迭代并返回最大元素,如清单 10.16所示。我们需要处理迭代器没有值的情况,在这种情况下max()将返回undefined。因此,我们不会使用for...of循环;相反,我们将使用next().

Now let’s implement a max() generic algorithm that expects an iterator over an IComparable set of values and returns the maximum element, as shown in listing 10.16. We will need to handle the case in which the iterator has no values, in which case max() will return undefined. For that reason, we won’t use a for...of loop; rather, we will advance the iterator manually by using next().

清单 10.16。max()算法
函数 max<T extends IComparable<T>>(iter: Iterable<T>)
    : 吨 | undefined {                                        1 
    let iterator: Iterator<T> = iter[Symbol.iterator]();    2个

    让当前:IteratorResult<T> = iterator.next();       3个

    如果(当前完成)返回未定义;                     4个

    让结果:T = current.value;                          5个

    而(真){
        current = iterator.next();

        如果(当前完成)返回结果;                    6个

        如果(current.value.compareTo(结果)==
            ComparisonResult.GreaterThan) {
            结果 = 当前值;                         7
        }
    }
}
function max<T extends IComparable<T>>(iter: Iterable<T>)
    : T | undefined {                                       1
    let iterator: Iterator<T> = iter[Symbol.iterator]();    2

    let current: IteratorResult<T> = iterator.next();       3

    if (current.done) return undefined;                     4

    let result: T = current.value;                          5

    while (true) {
        current = iterator.next();

        if (current.done) return result;                    6

        if (current.value.compareTo(result) ==
            ComparisonResult.GreaterThan) {
            result = current.value;                         7
        }
    }
}

  • 1 max() 对类型 T 施加 IComparable<T> 约束。
  • 1 max() puts an IComparable<T> constraint on the type T.
  • 2 我们从 Iterable<T> 参数中得到一个 Iterator<T>。
  • 2 We get an Iterator<T> from the Iterable<T> argument.
  • 3 我们调用 next() 一次以获得第一个值。
  • 3 We call next() once to get to the first value.
  • 4 如果没有第一个值,我们返回 undefined。
  • 4 In case there is no first value, we return undefined.
  • 5 我们将 result 初始化为迭代器返回的第一个值。
  • 5 We initialize result to be the first value returned by the iterator.
  • 6 当迭代器完成后,我们返回结果。
  • 6 When the iterator is done, we return the result.
  • 7 只要当前值大于当前存储的最大值,我们就用当前值更新结果。
  • 7 Whenever the current value is greater than the currently stored maximum, we update the result with the current value.

许多算法(例如max())需要它们所操作的类型中的某些东西。另一种方法是使比较成为函数本身的参数,而不是泛型类型约束。而不是IComparable<T>,max()可以期待第二个参数 - 一个compare()函数 - 从两个类型的参数T到 a ComparisonResult,如以下代码所示。

Many algorithms, such as max(), require certain things from the types they operate on. An alternative is to make the comparison an argument to the function itself as opposed to a generic type constraint. Instead of IComparable<T>, max() can expect a second argument—a compare() function—from two arguments of type T to a ComparisonResult, as shown in the following code.

清单 10.17。max()compare()带参数 的算法
函数 max<T>(iter: Iterable<T>,
     compare: (x: T, y: T) => ComparisonResult )      1
    : 吨 | 不明确的 {
    让迭代器: Iterator<T> = iter[Symbol.iterator]();

    让当前:IteratorResult<T> = iterator.next();

    如果(当前完成)返回未定义;

    让结果:T = current.value;

    而(真){
        current = iterator.next();

        如果(当前完成)返回结果;

        if ( compare(current.value, result) 
            == ComparisonResult.GreaterThan) {      2
            结果 = 当前值;
        }
    }
}
function max<T>(iter: Iterable<T>,
    compare: (x: T, y: T) => ComparisonResult)     1
    : T | undefined {
    let iterator: Iterator<T> = iter[Symbol.iterator]();

    let current: IteratorResult<T> = iterator.next();

    if (current.done) return undefined;

    let result: T = current.value;

    while (true) {
        current = iterator.next();

        if (current.done) return result;

        if (compare(current.value, result)
            == ComparisonResult.GreaterThan) {     2
            result = current.value;
        }
    }
}

  • 1 compare() 是一个接受两个 T 并返回一个 ComparisonResult 的函数。
  • 1 compare() is a function that takes two Ts and returns a ComparisonResult.
  • 2 我们调用 compare() 参数而不是 IComparable.compareTo() 方法。
  • 2 Instead of the IComparable.compareTo() method, we call the compare() argument.

这种实现的好处是类型T不再受限,我们可以插入任何比较函数。缺点是对于具有自然顺序的类型(数字、温度、距离等),我们必须继续显式提供比较函数。好的算法库通常提供两种版本的算法:一种使用类型的自然比较,另一种调用者可以提供自己的算法。

The advantage of this implementation is that the type T is no longer constrained, and we can plug in any comparison function. The disadvantage is that for types that have a natural order (numbers, temperatures, distances, and so on), we have to keep supplying a compare function explicitly. Good algorithm libraries usually provide both versions of an algorithm: one that uses a type’s natural comparison and another for which callers can supply their own.

算法对T它所操作的类型所提供的方法和属性了解得越多,它就越能在其实现中利用这些方法和属性。接下来,让我们看看算法如何使用迭代器来提供更高效的实现。

The more an algorithm knows about the methods and properties that a type T it operates on provides, the more it can leverage those in its implementation. Next, let’s see how algorithms can use iterators to provide more efficient implementations.

10.3.3。锻炼

10.3.3. Exercise

1个

clamp()实现一个接受值、低值和高值的通用函数。如果该值在低-高范围内,则返回值。如果该值小于低,则返回低。如果值大于 high,则返回 high。使用IComparable本节中定义的接口。

1

Implement a generic function clamp() that takes a value, a low, and a high. If the value is within the low-high range, it returns the values. If the value is less than low, it returns low. If the value is larger than high, it returns high. Use the IComparable interface defined in this section.

10.4. 使用迭代器的高效逆向和其他算法

10.4. Efficient reverse and other algorithms using iterators

到目前为止,我们已经了解了以线性方式处理序列的算法。map(), filter(), reduce(), 和max()all 从头到尾遍历一系列值。它们都在线性时间(与序列的大小成正比)和恒定空间内运行。(无论序列的大小如何,内存需求都是恒定的。)让我们看看另一种算法:reverse()

So far, we’ve looked at algorithms that process a sequence in a linear fashion. map(), filter(), reduce(), and max() all iterate over a sequence of values from start to finish. They all run in linear time (proportionate to the size of the sequence) and constant space. (Memory requirements are constant regardless of the size of the sequence.) Let’s look at another algorithm: reverse().

该算法采用一个序列并将其反转,使最后一个元素成为第一个元素,倒数第二个元素成为第二个元素,依此类推。一种实现方式是将其输入的所有元素压入堆栈,然后将它们弹出,如图10.1代码清单 10.18reverse()所示。

This algorithm takes a sequence and reverses it, making the last element the first one, the second-to-last element the second one, and so on. One way to implement reverse() is to push all elements of its input into a stack and then pop them out, as shown in figure 10.1 and listing 10.18.

图 10.1。用堆栈反转序列:原始序列中的元素被压入堆栈,然后弹出以产生反转序列。

清单 10.18。reverse()带堆栈
function *reverse<T>(iter: Iterable<T>): IterableIterator<T> {     1 
    let stack: T[] = [];                                          2个

    for (iter 的常量值) {
        堆栈.push(值);                                        3个
    }

    而(真){
        让值:T | 未定义 = stack.pop();                   4个

        如果(值==未定义)返回;                           5个

        屈服值;                                              6个
    }
}
function *reverse<T>(iter: Iterable<T>): IterableIterator<T> {    1
    let stack: T[] = [];                                          2

    for (const value of iter) {
        stack.push(value);                                        3
    }

    while (true) {
        let value: T | undefined = stack.pop();                   4

        if (value == undefined) return;                           5

        yield value;                                              6
    }
}

  • 1 reverse() 是一个生成器,遵循与我们其他算法相同的模式。
  • 1 reverse() is a generator, following the same pattern as our other algorithms.
  • 2 JavaScript 数组提供了 push() 和 pop() 方法,所以我们用一个作为栈。
  • 2 JavaScript arrays provide push() and pop() methods, so we use one as a stack.
  • 3 我们将序列中的所有值压入堆栈。
  • 3 We push all values from the sequence on the stack.
  • 4 我们从堆栈中弹出一个值;如果堆栈为空,则这是未定义的。
  • 4 We pop a value from the stack; this is undefined if the stack is empty.
  • 5 如果我们清空堆栈,返回,因为我们已经完成了。
  • 5 If we emptied the stack, return, as we are done.
  • 6 产生值并重复。
  • 6 Yield the value and repeat.

这个实现很简单,但不是最有效的。虽然它在线性时间运行,但它也需要线性空间。输入序列越大,该算法将其所有元素压入堆栈所需的内存就越多。

This implementation is straightforward but not the most efficient. Although it runs in linear time, it also requires linear space. The larger the input sequence, the more memory this algorithm will need to push all its elements onto the stack.

让我们暂时把迭代器放在一边,看看我们如何对数组实现高效的反转,如清单 10.19所示。我们可以就地执行此操作,从两端开始交换数组的元素,而不需要额外的堆栈(图 10.2)。

Let’s set iterators aside for now and look at how we would implement an efficient reverse over an array, as shown in listing 10.19. We can do this in-place, swapping the elements of the array starting from both ends without requiring an additional stack (figure 10.2).

图 10.2。通过交换元素来原地反转数组

清单 10.19。reverse()对于数组
function reverse<T>(values: T[]): void {      1 
    let begin: number = 0;                   2
    让结束:number = values.length;         2个


    while (begin < end) {                     3 
        const temp: T = values[begin];       4
        值[开始] = 值[结束 - 1];     4 个
        值 [end - 1] = temp;              4个

        开始++;                             5
        结束--;                               5个
    }
}
function reverse<T>(values: T[]): void {     1
    let begin: number = 0;                   2
    let end: number = values.length;         2


    while (begin < end) {                    3
        const temp: T = values[begin];       4
        values[begin] = values[end - 1];     4
        values[end - 1] = temp;              4

        begin++;                             5
        end--;                               5
    }
}

  • 1 这个版本的 reverse() 需要一个 Ts 数组,而不是一个 Iterable。
  • 1 This version of reverse() expects an array of Ts, not an Iterable.
  • 2 begin和end本来指向数组的首尾。
  • 2 begin and end originally point to the beginning and end of the array.
  • 3 重复,直到两者相遇或通过对方。
  • 3 Repeat until the two meet or pass each other.
  • 4 将 begin 的值与 end 的值 - 1 交换。(最初,end 是数组中最后一个元素之后的一个元素。)
  • 4 Swap the value at begin with the value at end - 1. (Originally, end was one element after the last one in the array.)
  • 5 增加开始索引并减少结束索引。
  • 5 Increment the begin index and decrement the end index.

如我们所见,此实现比前一个更有效。它仍然是线性时间,因为我们需要触及序列的每个元素(不可能在不接触每个元素的情况下反转序列),但它需要恒定的空间来运行。temp与需要与其输入一样大的堆栈的先前版本不同,这个版本使用type 的临时值T,无论输入有多大。

As we can see, this implementation is more efficient than the preceding one. It is still linear time, as we need to touch every element of the sequence (it is impossible to reverse a sequence without touching each element), but it requires constant space to run. Unlike the previous version, which needed a stack as large as its input, this one uses the temporary temp of type T, regardless of how large the input is.

我们可以概括这个例子并为任何数据结构提供有效的反向算法吗?我们可以,但我们需要调整迭代器的概念。Iterator<T>,Iterable<T>以及两者的组合 ,IterableIterator<T>是 TypeScript 在 JavaScript ES6 标准之上提供的接口。现在我们将超越这一点,看看一些不属于语言标准的迭代器。

Can we generalize this example and provide an efficient reverse algorithm for any data structure? We can, but we need to tweak our notion of iterators. Iterator<T>, Iterable<T>, and the combination of the two, IterableIterator<T>, are interfaces that TypeScript provides over the JavaScript ES6 standard. Now we’ll go beyond that and look at some iterators that are not part of the language standard.

10.4.1。迭代器构建块

10.4.1. Iterator building blocks

JavaScript 迭代器允许我们检索值并前进直到序列耗尽。如果我们想运行就地算法,我们需要更多的能力。我们不仅需要能够读取给定位置的值,还需要能够设置它们。在我们的reverse()例子中,我们从序列的两端开始,在中间结束,这意味着迭代器无法判断它何时完全由自己完成。我们知道当和相互传递reverse()时就完成了,所以我们需要一种方法来判断两个迭代器何时相同。 beginend

JavaScript iterators allow us to retrieve values and advance until the sequence is exhausted. If we want to run an in-place algorithm, we need a few more capabilities. We also need to be able not only to read values at a given position, but also set them. In our reverse() case, we start from both ends of the sequence and end in the middle, which means that an iterator can’t tell when it is done all by itself. We know that reverse() is done when begin and end pass each other, so we need a way to tell when two iterators are the same.

为了支持高效的算法,让我们将迭代器重新定义为一组接口,每个接口都描述了额外的功能。首先,让我们定义一个IReadable<T>公开get()返回 type 值的方法的方法T。我们将使用此方法从迭代器中读取值。我们还将定义一个IIncrementable<T>公开increment()可用于推进迭代器的方法的方法,如以下清单所示。

To support efficient algorithms, let’s redefine our iterators as a set of interfaces, each describing additional capabilities. First, let’s define an IReadable<T> that exposes a get() method returning a value of type T. We will use this method to read a value from an iterator. We’ll also define an IIncrementable<T> that exposes an increment() method we can use to advance our iterator, as the following listing shows.

清单 10.20。IReadable<T>IIncrementable<T>
接口 IReadable<T> {
    得到():T;                    1个
}

接口 IIncrementable<T> {
    增量():无效;           2 
}
interface IReadable<T> {
    get(): T;                    1
}

interface IIncrementable<T> {
    increment(): void;           2
}

  • 1 IReadable 声明了一个方法 get(),用于检索迭代器的当前值 T。
  • 1 IReadable declares a single method, get(), that retrieves the current value T of an iterator.
  • 2 IIncrementable 声明了一个方法 increment(),它将迭代器推进到下一个元素。
  • 2 IIncrementable declares a single method, increment(), that advances an iterator to the next element.

这两个接口几乎足以支持我们原有的线性遍历算法如map(). 最后缺少的是弄清楚什么时候应该停止。我们知道迭代器无法自行判断何时完成,因为有时它不需要遍历整个序列。我们将引入相等的概念:一个迭代器begin和一个迭代器end在指向同一个元素时是相等的。这是多比标准Iterator- <T>实现更灵活。我们可以初始化end为序列最后一个元素之后的一个元素。然后我们可以前进begin直到它等于end,在这种情况下我们将知道我们已经遍历了整个序列。但我们也可以end向后移动,直到它指向序列的第一个元素——这是我们无法用标准Iterator<T>. (图 10.3)。

These two interfaces are almost enough to support our original linear traversal algorithms such as map(). The last thing missing is figuring out when we should stop. We know that an iterator can’t tell by itself when it is done, as sometimes it doesn’t need to traverse the whole sequence. We’ll introduce the concept of equality: an iterator begin and an iterator end are equal when they point to the same element. This is much more flexible than the standard Iterator- <T> implementation. We can initialize end to be one element after the last element of a sequence. Then we can advance begin until it is equal to end, in which case we’ll know that we’ve traversed the whole sequence. But we can also move end back until it points to the first element of the sequence—something we couldn’t have done with the standard Iterator<T>. (figure 10.3).

图 10.3。beginend迭代器定义一个范围:begin指向第一个元素,并end指向最后一个元素。

让我们IInputIterator<T>在下一个清单中定义一个接口,它实现了IReadable<T>IIncrementable<T>,加上一个equals()我们可以用来比较两个迭代器的方法。

Let’s define an IInputIterator<T> interface in the next listing as an interface that implements both IReadable<T> and IIncrementable<T>, plus an equals() method we can use to compare two iterators.

清单 10.21。IInputIterator<T>
接口 IInputIterator<T> 扩展 IReadable<T>, IIncrementable<T> {
    等于(其他:IInputIterator<T>):布尔值;
}
interface IInputIterator<T> extends IReadable<T>, IIncrementable<T> {
    equals(other: IInputIterator<T>): boolean;
}

迭代器本身无法再确定它何时遍历了整个序列。一个序列现在由两个迭代器定义——一个指向序列开头的迭代器和一个指向序列最后一个元素之后的迭代器。

The iterator itself can no longer determine when it has traversed the whole sequence. A sequence is now defined by two iterators—an iterator pointing to the start of the sequence and an iterator pointing to one past the last element of the sequence.

有了这些可用的接口,让我们在下一个清单中更新第 9 章中的链表迭代器。我们的链表被实现为LinkedListNode<T>具有一个value属性的类型,一个next属性可以是列表中的最后一个LinkedListNode<T>节点undefined

With these interfaces available, let’s update our linked list iterator from chapter 9 in the next listing. Our linked list is implemented as the type LinkedListNode<T> with a value property and a next property that can be a LinkedListNode<T> or undefined for the last node in the list.

清单 10.22。链表实现
类 LinkedListNode<T> {
    值:T;
    下一个:LinkedListNode<T> | 不明确的;

    构造函数(值:T){
        this.value = 值;
    }
}
class LinkedListNode<T> {
    value: T;
    next: LinkedListNode<T> | undefined;

    constructor(value: T) {
        this.value = value;
    }
}

让我们看看我们如何在下面的清单中为这个链表的一对迭代器建模。首先,我们需要实现一个LinkedListInputIterator<T>满足IInputIterator<T>链表新接口的 。

Let’s see how we can model a pair of iterators over this linked list in the following listing. First, we’ll need to implement a LinkedListInputIterator<T> that satisfies our new IInputIterator<T> interface for a linked list.

清单 10.23。链表输入迭代器
类 LinkedListInputIterator<T> 实现 IInputIterator<T> {
    私有节点:LinkedListNode<T> | 不明确的;
    构造函数(节点:LinkedListNode<T> | 未定义){
        this.node = 节点;
    }

    增量():无效{                                                  1
        如果(!this.node)抛出错误();

        this.node = this.node.next;
    }

    得到():T {                                                           2
        如果(!this.node)抛出错误();

        返回这个节点值;
    }

    等于(其他:IInputIterator<T>):布尔值 {                          3
        return this.node == (<LinkedListInputIterator<T>>other).node;
    }
}
class LinkedListInputIterator<T> implements IInputIterator<T> {
    private node: LinkedListNode<T> | undefined;
    constructor(node: LinkedListNode<T> | undefined) {
        this.node = node;
    }

    increment(): void {                                                 1
        if (!this.node) throw Error();

        this.node = this.node.next;
    }

    get(): T {                                                          2
        if (!this.node) throw Error();

        return this.node.value;
    }

    equals(other: IInputIterator<T>): boolean {                         3
        return this.node == (<LinkedListInputIterator<T>>other).node;
    }
}

  • 1 如果当前节点未定义,抛出错误;否则,前进到下一个节点。
  • 1 If the current node is undefined, throw an error; otherwise, advance to the next node.
  • 2 如果当前节点未定义,则抛出错误;否则,获取它的值。
  • 2 If the current node is undefined, throw an error; otherwise, get its value.
  • 3 如果迭代器包装相同的节点,则迭代器被认为是相等的。我们可以强制转换为 LinkedListInputIterator<T>,因为调用者不应比较不同类型的迭代器。
  • 3 Iterators are considered to be equal if they wrap the same node. We can cast to LinkedListInputIterator<T> because callers shouldn’t compare iterators of different types.

begin现在我们可以通过初始化为列表的头部和endbe 来在链表上创建一对迭代器undefined,如以下代码所示。

Now we can create a pair of iterators over a linked list by initializing begin to be the head of the list and end to be undefined, as shown in the following code.

清单 10.24。链表上的一对迭代器
const head: LinkedListNode<number> = new LinkedListNode(0);                1个
head.next = new LinkedListNode(1);
head.next.next = new LinkedListNode(2);

让我们开始吧: IInputIterator<number> = new LinkedListInputIterator(head);     2
让结束:IInputIterator<number> = new LinkedListInputIterator(undefined);  3个
const head: LinkedListNode<number> = new LinkedListNode(0);                1
head.next = new LinkedListNode(1);
head.next.next = new LinkedListNode(2);

let begin: IInputIterator<number> = new LinkedListInputIterator(head);     2
let end: IInputIterator<number> = new LinkedListInputIterator(undefined);  3

  • 1 一个有几个节点的列表
  • 1 A list with a few nodes
  • 2 begin 是作为参数传递的链表的头部。
  • 2 begin is the head of the linked list passed as an argument.
  • 3 端未定义。
  • 3 end is undefined.

我们称其为输入迭代器,因为我们可以使用该方法从中读取值get()

We call this an input iterator because we can read values from it by using the get() method.

输入迭代器

输入迭代器是可以遍历序列一次并提供其值的迭代器。它无法再次重播这些值,因为这些值可能不再可用。输入迭代器不必遍历持久数据结构;它还可以提供来自生成器或其他来源的值(图 10.4)。

An input iterator is an iterator that can traverse a sequence once and provide its values. It can’t replay the values a second time, as the values may no longer be available. An input iterator doesn’t have to traverse a persistent data structure; it can also provide values from a generator or some other source (figure 10.4).

图 10.4。输入迭代器可以检索当前元素的值并前进到下一个元素。

让我们也定义一个输出迭代器作为我们可以写入的迭代器。为此,我们将声明一个IWritable<T>带有方法的接口set(),并将、和方法IOutput-Iterator<T>组合起来,如下一个清单所示。 IWritable<T>IIncrementable<T>equals()

Let’s also define an output iterator as an iterator we can write to. For that, we’ll declare an IWritable<T> interface with a set() method and have our IOutput-Iterator<T> be the combination of IWritable<T>, IIncrementable<T>, and an equals() method, as shown in the next listing.

清单 10.25。IWritable<T>IOutputIterator<T>
接口 IWritable<T> {
    设置(值:T):无效;
}
接口 IOutputIterator<T> 扩展 IWritable<T>, IIncrementable<T> {
    等于(其他:IOutputIterator<T>):布尔值;
}
interface IWritable<T> {
    set(value: T): void;
}
interface IOutputIterator<T> extends IWritable<T>, IIncrementable<T> {
    equals(other: IOutputIterator<T>): boolean;
}

我们可以将值写入这种类型的迭代器,但我们不能读回它们。

We can write values to this type of iterator, but we can’t read them back.

输出迭代器

输出迭代器是可以遍历序列并向其写入值的迭代器;它不必能够读回它们。输出迭代器不必遍历持久数据结构;它还可以将值写入其他输出。

An output iterator is an iterator that can traverse a sequence and write values to it; it doesn’t have to be able to read them back. An output iterator doesn’t have to traverse a persistent data structure; it can also write values to other outputs.

让我们实现一个写入控制台的输出迭代器。写入输出流是输出迭代器最常见的用例:那是我们可以输出数据但无法读回的时候。我们可以将数据(但无法读取)写入网络连接、标准输出、标准错误等。在我们的例子中,推进迭代器不做任何事情,而设置一个值 calls console.log(),如下一个清单所示。

Let’s implement an output iterator that writes to the console. Writing to an output stream is the most common use case for an output iterator: that’s when we can output data but can’t read it back. We can write data (without being able to read it) to a network connection, standard output, standard error, and so on. In our case, advancing the iterator doesn’t do anything, whereas setting a value calls console.log(), as shown in the next listing.

清单 10.26。控制台输出迭代器
类 ConsoleOutputIterator<T> 实现 IOutputIterator<T> {
    设置(值:T):无效{
        控制台日志(值);                       1个
    }

    增量():无效{}                          2

    等于(其他:IOutputIterator<T>):布尔值{
        返回假;                             3个
    }
}
class ConsoleOutputIterator<T> implements IOutputIterator<T> {
    set(value: T): void {
        console.log(value);                       1
    }

    increment(): void { }                         2

    equals(other: IOutputIterator<T>): boolean {
        return false;                             3
    }
}

  • 1 set() 记录到控制台。
  • 1 set() logs to the console.
  • 2 increment() 不需要做任何事情,因为在这种情况下我们没有遍历数据结构。
  • 2 increment() doesn’t have to do anything because we’re not traversing a data structure in this case.
  • 3 equals() 可以安全地始终返回 false,因为写入控制台没有结束比较。
  • 3 equals() can safely always return false, as writing to the console doesn’t have an end to compare against.

现在我们有一个接口,它描述了一个输入迭代器和一个在我们的链表上实现的具体实例。我们还有一个描述输出迭代器的接口和一个记录到控制台的具体实现。有了这些部分,我们可以提供清单 10.27map()中的替代实现。

Now we have an interface that describes an input iterator and a concrete instance of an implementation over our linked list. We also have an interface that describes an output iterator and a concrete implementation that logs to the console. With these pieces in place, we can provide an alternative implementation of map() in listing 10.27.

这个新版本的map()将接受一对定义序列的输入迭代器和一个输出迭代器作为参数beginend它将out在其中写入将给定函数映射到序列上的结果。因为我们不再使用标准的 JavaScript,所以我们失去了一些语法糖——没有yield也没有for...of循环。

This new version of map() will take as argument a pair of begin and end input iterators that define a sequence and an output iterator out, where it will write the results of mapping the given function over the sequence. Because we are no longer using standard JavaScript, we lose some of the syntactic sugar—no yield and no for...of loops.

清单 10.27。map()带有输入和输出迭代器
函数映射<T, U>(
    开始:IInputIterator<T>,结束:IInputIterator<T>,     1
    输出:IOutputIterator<U>,                              2
    函数:(值:T)=> U):void {

    while (!begin.equals(end)) {                          3 
        out.set(func(begin.get()));                      4个

        begin.increment();                               5
        输出增量();                                 5个
    }
}
function map<T, U>(
    begin: IInputIterator<T>, end: IInputIterator<T>,    1
    out: IOutputIterator<U>,                             2
    func: (value: T) => U): void {

    while (!begin.equals(end)) {                         3
        out.set(func(begin.get()));                      4

        begin.increment();                               5
        out.increment();                                 5
    }
}

  • 1 开始和结束迭代器定义输入序列。
  • 1 begin and end iterators define the input sequence.
  • 2 out 是函数结果的输出迭代器。
  • 2 out is an output iterator for the result of the function.
  • 3 重复直到我们遍历整个序列并且开始变成结束。
  • 3 Repeat until we traverse the whole sequence and begin becomes end.
  • 4 输出将函数应用于当前元素的结果。
  • 4 Output the result of applying the function to the current element.
  • 5 递增输入和输出迭代器。
  • 5 Increment both the input and the output iterators.

这个版本和map()基于native的一样通用Iterable-Iterator<T>:我们可以提供any IInputIterator<T>,遍历链表的,按顺序遍历树的,等等。我们还可以提供任何一种IOutput-Iterator<T>写入控制台或写入数组的方式。

This version of map() is as general as the one based on the native Iterable-Iterator<T>: we can provide any IInputIterator<T>, one that traverses a linked list, one that traverses a tree in order, and so on. We can also provide any IOutput-Iterator<T>—one that writes to the console or one that writes to an array.

到目前为止,这并没有给我们带来太多好处。我们有一个替代实现,它不能利用 TypeScript 提供的特殊语法。但这些迭代器只是基本的构建块。我们可以定义更强大的迭代器,接下来我们将看看这些。

So far, this doesn’t gain us much. We have an alternative implementation that can’t leverage the special syntax that TypeScript provides. But these iterators are just the basic building blocks. We can define more-powerful iterators, and we’ll look at these next.

10.4.2。一个有用的 find()

10.4.2. A useful find()

让我们采用另一种常见的算法:find()。该算法采用一系列值和一个谓词,并返回谓词返回的第一个元素 true。我们可以使用标准来实现这一点Iterable<T>,如以下清单所示。

Let’s take another common algorithm: find(). This algorithm takes a sequence of values and a predicate, and returns the first element for which the predicate returns true. We can implement this by using the standard Iterable<T>, as the following listing shows.

清单 10.28。find()可迭代
函数 find<T>(iter: Iterable<T>,
    pred: (值: T) => 布尔值: T | 不明确的 {
    for (iter 的常量值) {
        如果(预测值(值)){
            返回值;
        }
    }

    返回未定义;
}
function find<T>(iter: Iterable<T>,
    pred: (value: T) => boolean): T | undefined {
    for (const value of iter) {
        if (pred(value)) {
            return value;
        }
    }

    return undefined;
}

这有效,但不是那么有用。如果我们想在找到它之后更改该值怎么办?如果我们在数字链表中搜索第一次出现的 42 以便我们可以用 0 替换它,返回 42 对我们没有帮助。结果也可能是find()a boolean,因为这个函数只告诉我们是否该值存在于序列中。

This works, but it’s not that useful. What if we want to change the value after we find it? If we are searching over a linked list of numbers for the first occurrence of 42 so that we can replace it with 0, it doesn’t help us that find() returns 42. The result may as well be a boolean, as this function tells us only whether the value exists in the sequence.

如果我们不返回值本身,而是得到一个指向该值的迭代器呢?开箱即用的 JavaScriptIterator<T>是只读的。我们已经看到了如何创建一个迭代器,我们也可以通过它来设置值。对于这种情况,我们需要可读和可写迭代器的组合。让我们定义一个前向迭代器。

What if, instead of returning the value itself, we get an iterator pointing to that value? The out-of-the-box JavaScript Iterator<T> is read-only. We’ve seen how to create an iterator through which we can also set values. For this scenario, we’ll need a combination of readable and writable iterators. Let’s define a forward iterator.

前向迭代器

前向迭代器是可以前进的迭代器,可以读取其当前位置的值,并更新该值。前向迭代器也可以被克隆,因此推进迭代器的一个副本不会推进克隆。这很重要,因为它允许我们多次遍历一个序列,这与输入和输出迭代器不同(图 10.5)。

A forward iterator is an iterator that can be advanced, can read the value at its current position, and update that value. A forward iterator can also be cloned, so that advancing one copy of the iterator does not advance the clone. This is important, as it allows us to traverse a sequence multiple times, unlike input and output iterators (figure 10.5).

图 10.5。前向迭代器可以读取和写入当前元素的值,前进到下一个元素,并创建一个支持多次遍历的自身克隆。在此图中,我们看到了如何clone()创建迭代器的副本。当我们推进原件时,克隆件不会移动。

下一个清单中显示的界面是、、、 和方法IForwardIterator<T>的组合。 IReadable<T>IWritable<T>IIncrementable<T>equals()clone()

Our IForwardIterator<T> interface shown in the next listing is a combination of IReadable<T>, IWritable<T>, IIncrementable<T>, and the equals() and clone() methods.

清单 10.29。IForwardIterator<T>
接口 IForwardIterator<T> 扩展
    IReadable<T>, IWritable<T>, IIncrementable<T> {
    等于(其他: IForwardIterator<T>):布尔值;
    克隆(): IForwardIterator<T>;
}
interface IForwardIterator<T> extends
    IReadable<T>, IWritable<T>, IIncrementable<T> {
    equals(other: IForwardIterator<T>): boolean;
    clone(): IForwardIterator<T>;
}

例如,让我们实现接口以迭代以下清单中的链表。我们将更新我们的LinkedListIterator<T>以提供新界面所需的其他方法。

As an example, let’s implement the interface to iterate over our linked list in the following listing. We’ll update our LinkedListIterator<T> to also provide the additional methods required by our new interface.

清单 10.30。LinkedListIterator<T>实施IForwardIterator<T>
类 LinkedListIterator<T> 实现IForwardIterator<T> {        1
    私有节点:LinkedListNode<T> | 不明确的;

    构造函数(节点:LinkedListNode<T> | 未定义){
        this.node = 节点;
    }

    增量():无效{
        如果(!this.node)返回;
        this.node = this.node.next;
    }

    得到():T {
        如果(!this.node)抛出错误();

        返回这个节点值;
    }

    set(value: T): void {                                           2
         if (!this.node) throw Error();

        this.node.value = 值;
    }

    equals(other: IForwardIterator<T> ): boolean {                   3
        返回 this.node == (<LinkedListIterator<T>>other).node;
    }

    clone(): IForwardIterator<T> {                                  4
         return new LinkedListIterator(this.node); 
    } 
}
class LinkedListIterator<T> implements IForwardIterator<T> {       1
    private node: LinkedListNode<T> | undefined;

    constructor(node: LinkedListNode<T> | undefined) {
        this.node = node;
    }

    increment(): void {
        if (!this.node) return;
        this.node = this.node.next;
    }

    get(): T {
        if (!this.node) throw Error();

        return this.node.value;
    }

    set(value: T): void {                                          2
        if (!this.node) throw Error();

        this.node.value = value;
    }

    equals(other: IForwardIterator<T>): boolean {                  3
        return this.node == (<LinkedListIterator<T>>other).node;
    }

    clone(): IForwardIterator<T> {                                 4
        return new LinkedListIterator(this.node);
    }
}

  • 1 此版本的 LinkedListIterator<T> 实现了新的 IForwardIterator<T> 接口。
  • 1 This version of LinkedListIterator<T> implements the new IForwardIterator<T> interface.
  • 2 set() 是 IWritable<T> 所需的附加方法,用于更新链表节点的值。
  • 2 set() is an additional method required by IWritable<T> that updates the value of a linked list node.
  • 3 equals() 现在需要另一个 IForwardIterator<T>。
  • 3 equals() now expects another IForwardIterator<T>.
  • 4 clone() 创建一个新的迭代器,指向与该迭代器相同的节点。
  • 4 clone() creates a new iterator pointing to the same node as this iterator.

现在让我们看一下find()它的一个版本,它接受一对beginandend迭代器,并返回一个指向第一个满足谓词的元素的迭代器,如下一个清单所示。有了这个版本,我们可以在找到它时更新它。

Now let’s look at a version of find() that takes a pair of begin and end iterators, and returns an iterator pointing to the first element satisfying the predicate, shown in the next listing. With this version, we can update the value when we find it.

清单 10.31。find()带前向迭代器
函数查找<T>(
    开始: IForwardIterator<T>, 结束: IForwardIterator<T>,     1 
    pred: (value: T) => boolean): IForwardIterator<T> {       2 
    while (!begin.equals(end)) {                              3
        如果(预测(开始。得到())){
            回归开始;                                    4个
        }

        begin.increment();                                   5个
    }

    返回端;                                              6 
}
function find<T>(
    begin: IForwardIterator<T>, end: IForwardIterator<T>,    1
    pred: (value: T) => boolean): IForwardIterator<T> {      2
    while (!begin.equals(end)) {                             3
        if (pred(begin.get())) {
            return begin;                                    4
        }

        begin.increment();                                   5
    }

    return end;                                              6
}

  • 1 开始和结束前向迭代器定义序列。
  • 1 begin and end forward iterators define the sequence.
  • 2 该函数返回指向找到的元素的前向迭代器。
  • 2 The function returns a forward iterator pointing to the found element.
  • 3 重复直到遍历整个序列。
  • 3 Repeat until we traverse the whole sequence.
  • 4 如果找到了要查找的元素,则返回迭代器。
  • 4 If we found the element we were looking for, return the iterator.
  • 5 递增迭代器并前进到序列中的下一个元素。
  • 5 Increment iterator and advance to the next element in the sequence.
  • 6 如果我们已经到达终点,我们还没有找到一个元素。我们返回结束迭代器。
  • 6 If we’ve reached the end, we haven’t found an element. We return the end iterator.

让我们使用数字链表,我们刚刚实现的迭代器遍历链表,并应用此算法找到第一个等于的值42并将其替换为 a 0,如以下代码所示。

Let’s use a linked list of numbers, the iterator we just implemented to traverse a linked list, and apply this algorithm to find the first value equal to 42 and replace it with a 0, as shown in the following code.

清单 10.32。在链接列表中 替换420
让 head: LinkedListNode<number> = new LinkedListNode(1);     1个
head.next = new LinkedListNode(2);
head.next.next = new LinkedListNode(42);

让我们开始吧: IForwardIterator<number> =
    新的链表迭代器(头);                             2个
让结束: IForwardIterator<number> =
    新的链表迭代器(未定义);                        2个

让 iter: IForwardIterator<number> =
    查找(开始,结束,(值:数字)=> 值 == 42);         3个

如果 (!iter.equals(end)) {                                       4 
    iter.set(0);                                              5 
}
let head: LinkedListNode<number> = new LinkedListNode(1);     1
head.next = new LinkedListNode(2);
head.next.next = new LinkedListNode(42);

let begin: IForwardIterator<number> =
    new LinkedListIterator(head);                             2
let end: IForwardIterator<number> =
    new LinkedListIterator(undefined);                        2

let iter: IForwardIterator<number> =
    find(begin, end, (value: number) => value == 42);         3

if (!iter.equals(end)) {                                      4
    iter.set(0);                                              5
}

  • 1 创建一个包含序列 1、2、42 的链表。
  • 1 Create a linked list containing the sequence 1, 2, 42.
  • 2 初始化链表的开始和结束前向迭代器。
  • 2 Initialize begin and end forward iterators for the linked list.
  • 3 调用 find 并获取指向值为 42 的第一个节点的迭代器。
  • 3 Call find and get an iterator to the first node with the value 42.
  • 4 我们需要确保我们找到了一个值为 42 的节点;否则,我们已经过了列表的末尾。
  • 4 We need to ensure that we found a node with value 42; otherwise, we are past the end of the list.
  • 5 如果我们这样做了,我们可以将其值更新为 0。
  • 5 If we did, we can update its value to 0.

前向迭代器非常强大,因为它们可以遍历序列任意次数并修改它。这个特性允许我们实现不需要复制整个数据序列来转换它的就地算法。最后,让我们来解决一下我们在本节开始时使用的算法:reverse()

Forward iterators are extremely powerful, as they can traverse a sequence any number of times and also modify it. This feature allows us to implement in-place algorithms that don’t need to copy over a whole sequence of data to transform it. Finally, let’s tackle the algorithm with which we started this section: reverse().

10.4.3。高效的 reverse()

10.4.3. An efficient reverse()

正如我们在数组实现中看到的,就地reverse()从数组的两端开始并交换元素,增加前面的索引并减少后面的索引,直到两者交叉。

As we saw in the array implementation, an in-place reverse() starts from both ends of the array and swaps elements, incrementing the front index and decrementing the back index until the two cross.

我们可以将数组实现概括为适用于任何序列,但是我们的迭代器需要一个额外的功能:递减其位置的能力。具有这种能力的迭代器称为双向迭代器

We can generalize the array implementation to work with any sequence, but we need one extra capability on our iterator: the ability to decrement its position. An iterator with this ability is called a bidirectional iterator.

双向迭代器

双向迭代器具有与正向迭代器相同的功能;此外,它可以递减。换句话说,双向迭代器可以向前和向后遍历一个序列(图 10.6)。

A bidirectional iterator has the same capabilities as a forward iterator; additionally, it can be decremented. In other words, a bidirectional iterator can traverse a sequence both forward and backward (figure 10.6).

图 10.6。双向迭代器可以读取和写入当前元素的值、克隆自身以及向前和向后移动。

让我们定义一个IBidirectionalIterator<T>类似于IForward-Iterator<T>带有附加decrement()方法的接口的接口。注意并不是所有的数据结构都能支持这样的迭代器,比如我们的链表。因为一个节点只引用它的后继节点,所以我们不能向后移动到前一个节点。但是我们可以在双向链表上提供一个双向迭代器,其中一个节点持有对其后继者和前任者或数组的引用。让我们在下一个清单中 实现一个ArrayIterator<T>as 。IBidirectionalIterator<T>

Let’s define an IBidirectionalIterator<T> interface similar to IForward-Iterator<T> interface with an additional decrement() method. Note that not all data structures can support such an iterator, such as our linked list. Because a node has a reference only to its successor, we cannot move backward to the preceding node. But we can provide a bidirectional iterator over a doubly linked list, in which a node holds references to both its successor and its predecessor or an array. Let’s implement an ArrayIterator<T> as an IBidirectionalIterator<T> in the next listing.

清单 10.33。IBidirectionalIterator<T>ArrayIterator<T>
接口 IBidirectionalIterator<T> 扩展
    IReadable<T>, IWritable<T>, IIncrementable<T> {
    递减():无效;                                  1个
    等于(其他:IBidirectionalIterator<T>):布尔值;
    克隆():IBidirectionalIterator<T>;
}

类 ArrayIterator<T> 实现 IBidirectionalIterator<T> {
    私有数组:T[];
    私有索引:数字;

    构造函数(数组:T[],索引:数字){
        this.array = 数组;
        this.index = 索引;
    }

    得到():T {
        返回 this.array[this.index];
    }

    设置(值:T):无效{
        this.array[this.index] = 值;
    }

    增量():无效{
        这个。索引++;
    }

    递减():无效{
        这个.index--;
    }

    等于(其他:IBidirectionalIterator<T>):布尔值{
        返回 this.index == (<ArrayIterator<T>>other).index;
    }

    克隆():IBidirectionalIterator<T>{
        返回新的 ArrayIterator(this.array, this.index);
    }
}
interface IBidirectionalIterator<T> extends
    IReadable<T>, IWritable<T>, IIncrementable<T> {
    decrement(): void;                                  1
    equals(other: IBidirectionalIterator<T>): boolean;
    clone(): IBidirectionalIterator<T>;
}

class ArrayIterator<T> implements IBidirectionalIterator<T> {
    private array: T[];
    private index: number;

    constructor(array: T[], index: number) {
        this.array = array;
        this.index = index;
    }

    get(): T {
        return this.array[this.index];
    }

    set(value: T): void {
        this.array[this.index] = value;
    }

    increment(): void {
        this.index++;
    }

    decrement(): void {
        this.index--;
    }

    equals(other: IBidirectionalIterator<T>): boolean {
        return this.index == (<ArrayIterator<T>>other).index;
    }

    clone(): IBidirectionalIterator<T> {
        return new ArrayIterator(this.array, this.index);
    }
}

  • 1 与 IForwardIterator<T> 相比,IBidirectioanlIterator<T> 多了一个 decrement() 方法。
  • 1 IBidirectioanlIterator<T> has an extra decrement() method compared with IForwardIterator<T>.

现在让我们reverse()根据一对beginend双向迭代器来实现。当两个迭代器相遇时,我们将交换值、增量begin、减量和停止。end我们必须确保两个迭代器永远不会相互传递,所以一旦我们移动其中一个,我们就会检查它们是否相遇。

Now let’s implement reverse() in terms of a pair of begin and end bidirectional iterators. We will swap the values, increment begin, decrement end, and stop when the two iterators meet. We must make sure that the two iterators never pass each other, so as soon as we move one of them, we check whether they met.

清单 10.34。reverse()带双向迭代器
函数反转<T>(
    开始:IBidirectionalIterator<T>,结束:IBidirectionalIterator<T>
    ): 空白 {
    while (!begin.equals(end)) {             1 
        end.decrement();                    2 
        if (begin.equals(end)) 返回;      3个

        const temp: T = begin.get();        4 
        begin.set(end.get());               4 
        end.set(temp);                      4个

        begin.increment();                  5个
    }
}
function reverse<T>(
    begin: IBidirectionalIterator<T>, end: IBidirectionalIterator<T>
    ): void {
    while (!begin.equals(end)) {            1
        end.decrement();                    2
        if (begin.equals(end)) return;      3

        const temp: T = begin.get();        4
        begin.set(end.get());               4
        end.set(temp);                      4

        begin.increment();                  5
    }
}

  • 1 重复直到开始和结束相遇。
  • 1 Repeat until begin and end meet.
  • 2 递减结束。请记住,end 从数组末尾后的一个元素开始,因此我们需要在使用它之前将其递减。
  • 2 Decrement end. Remember that end starts at one element past the end of the array, so we need to decrement it before using it.
  • 3 再次检查递减结束没有让两个迭代器指向同一个元素。
  • 3 Check again that decrementing end didn’t get the two iterators pointing to the same element.
  • 4 交换值。
  • 4 Swap the values.
  • 5 最后,递增开始,然后重复。(while 循环条件再次检查两个迭代器是否相遇。)
  • 5 Finally, increment start and then repeat. (The while loop condition checks again whether the two iterators met.)

让我们在以下清单中的一组数字上尝试一下。

Let’s try it out on an array of numbers in the following listing.

清单 10.35。反转数字数组
让数组:number[] = [1, 2, 3, 4, 5];

让我们开始:IBidirectionalIterator<number>
    = 新的数组迭代器(数组,0);               1个
让结束:IBidirectionalIterator<number>
    = new ArrayIterator(array, array.length);    2个

反向(开始,结束);

控制台日志(数组);                              3个
let array: number[] = [1, 2, 3, 4, 5];

let begin: IBidirectionalIterator<number>
    = new ArrayIterator(array, 0);               1
let end: IBidirectionalIterator<number>
    = new ArrayIterator(array, array.length);    2

reverse(begin, end);

console.log(array);                              3

  • 1 在索引 0 处初始化数组。
  • 1 Initialize begin over the array at index 0.
  • 2 在数组的索引长度处(最后一个元素之后)初始化结束。
  • 2 Initialize end over the array at index length (one past the last element).
  • 3 这将记录 [5, 4, 3, 2, 1]。
  • 3 This will log [5, 4, 3, 2, 1].

使用双向迭代器,我们可以概括出一种高效的就地reverse()处理任何我们可以在两个方向上遍历的数据结构。我们扩展了原始算法,该算法仅限于数组以处理任何IBidirectional-Iterator<T>. 我们可以应用相同的算法来反转双向链表和我们可以前后移动迭代器的任何其他数据结构。

Using bidirectional iterators, we can generalize an efficient, in-place reverse() to work on any data structure that we can traverse in two directions. We extended the original algorithm, which was limited to arrays to work with any IBidirectional-Iterator<T>. We can apply the same algorithm to reverse a doubly linked list and any other data structure over which we can move an iterator backward and forward.

请注意,我们当然也可以反转单链表,但这样的算法不能泛化。当我们反转一个单链表时,我们改变了结构,因为我们将对每个下一个元素的引用翻转为引用前一个元素。这样的算法与其操作的数据结构紧密耦合,无法推广。相比之下,我们reverse()需要双向迭代器的泛型对于任何可以提供此类迭代器的数据结构都以相同的方式工作。

Note that we can also reverse a singly linked list, of course, but such an algorithm does not generalize. When we reverse a singly linked list, we alter the structure, as we’re flipping references to each next element to refer to the previous element instead. Such an algorithm is tightly coupled to the data structure it operates on and can’t be generalized. By contrast, our generic reverse() that requires a bidirectional iterator works the same way for any data structure that can provide such an iterator.

10.4.4。高效的元素检索

10.4.4. Efficient element retrieval

有些算法对它们的迭代器的要求比对increment()和 的要求更多decrement()。一个很好的例子是排序算法。一个高效的 O(n log n) 排序,如快速排序,将不得不绕过它正在排序的数据结构,访问任意位置的元素。为此,双向迭代器是不够的。我们需要一个随机访问迭代器。

There are algorithms that require more from their iterators than increment() and decrement(). A good example is sorting algorithms. An efficient, O(n log n) sort such as quicksort will have to jump around the data structure that it is sorting, accessing elements at arbitrary locations. For this purpose, a bidirectional iterator is not enough. We need a random-access iterator.

随机访问迭代器

随机访问迭代器可以在常数时间内向前和向后跳转任意给定数量的元素。与可以一次递增或递减一步的双向迭代器不同,随机访问迭代器可以移动任意数量的元素(图 10.7)。

A random-access iterator can jump forward and backward any given number of elements in constant time. Unlike a bidirectional iterator, which can be incremented or decremented one step at a time, a random-access iterator can move any number of elements (figure 10.7).

图 10.7。随机访问迭代器可以读取和写入当前元素的值、克隆自身以及向后或向前移动任意数量的步骤。

数组是随机可访问数据结构的一个很好的例子,我们可以在其中索引并快速检索任何元素。相比之下,对于双向链表,我们需要遍历后继或前导引用才能到达元素。双向链表不支持随机访问迭代器。

Arrays are good examples of random-accessible data structures, in which we can index and quickly retrieve any element. By contrast, with a doubly linked list, we need to traverse through successor or predecessor references to reach an element. A doubly linked list cannot support a random-access iterator.

让我们将 an 定义IRandomAccessIterator<T>为一个迭代器,它不仅支持 的所有功能IBidirectionalIterator<T>,而且还move()支持移动迭代器 n 个元素的方法。对于随机访问迭代器,判断两个迭代器之间的距离也很有用。我们将distance()在下面的清单中添加一个返回两个迭代器之间差异的方法。

Let’s define an IRandomAccessIterator<T> as an iterator that supports not only all the capabilities of IBidirectionalIterator<T>, but also a move() method that moves the iterator n elements. With random access iterators, it’s also useful to tell how far apart two iterators are. We will add a distance() method that returns the difference between two iterators in the following listing.

清单 10.36。IRandomAccessIterator<T>
接口 IRandomAccessIterator<T>
    扩展 IReadable<T>, IWritable<T>, IIncrementable<T> {
    递减():无效;
    等于(其他:IRandomAccessIterator<T>):布尔值;
    克隆(): IRandomAccessIterator<T> ;
    移动(n:数字):无效;
    距离(其他:IRandomAccessIterator<T>):数字;
}
interface IRandomAccessIterator<T>
    extends IReadable<T>, IWritable<T>, IIncrementable<T> {
    decrement(): void;
    equals(other: IRandomAccessIterator<T>): boolean;
    clone(): IRandomAccessIterator<T>;
    move(n: number): void;
    distance(other: IRandomAccessIterator<T>): number;
}

让我们ArrayIterator<T>在下一个清单中更新我们的实现IRandom-AccessIterator<T>

Let’s update our ArrayIterator<T> in the next listing to implement IRandom-AccessIterator<T>.

清单 10.37。ArrayIterator<T>实现随机访问迭代器
类 ArrayIterator<T> 实现IRandomAccessIterator<T> {
    私有数组:T[];
    私有索引:数字;

    构造函数(数组:T[],索引:数字){
        this.array = 数组;
        this.index = 索引;
    }

    得到():T {
        返回 this.array[this.index];
    }

    设置(值:T):无效{
        this.array[this.index] = 值;
    }

    增量():无效{
        这个。索引++;
    }

    递减():无效{
        这个.index--;
    }

    等于(其他:IRandomAccessIterator<T>):布尔值{
        返回 this.index == (<ArrayIterator<T>>other).index;
    }

    克隆():IRandomAccessIterator<T> {
        返回新的 ArrayIterator(this.array, this.index);
    }

    移动(n:数字):void { 
        th​​is.index += n;                                          1
     }

    distance(other: IRandomAccessIterator<T>): number {           2
         return this.index - (<ArrayIterator<T>>other).index; 
    } 
}
class ArrayIterator<T> implements IRandomAccessIterator<T> {
    private array: T[];
    private index: number;

    constructor(array: T[], index: number) {
        this.array = array;
        this.index = index;
    }

    get(): T {
        return this.array[this.index];
    }

    set(value: T): void {
        this.array[this.index] = value;
    }

    increment(): void {
        this.index++;
    }

    decrement(): void {
        this.index--;
    }

    equals(other: IRandomAccessIterator<T>): boolean {
        return this.index == (<ArrayIterator<T>>other).index;
    }

    clone(): IRandomAccessIterator<T> {
        return new ArrayIterator(this.array, this.index);
    }

    move(n: number): void {
        this.index += n;                                         1
    }

    distance(other: IRandomAccessIterator<T>): number {          2
        return this.index - (<ArrayIterator<T>>other).index;
    }
}

  • 1 move() 使迭代器前进 n 步。(n 可以是负数向后移动。)
  • 1 move() advances the iterator n steps. (n can be negative to move backward.)
  • 2 distance() 确定两个迭代器之间的距离
  • 2 distance() determines the distance between two iterators

让我们采用一个受益于随机访问迭代器的非常简单的算法elementAt()begin该算法将 a和end定义序列和数字 n 的迭代器作为参数。end如果 n 大于序列的长度, 它将返回一个迭代器到序列的第 n 个元素或迭代器。

Let’s take a very simple algorithm that benefits from a random-access iterator: elementAt(). This algorithm takes as arguments a begin and end iterator defining a sequence and a number n. It will return an iterator to the nth element of the sequence or the end iterator if n is larger than the length of the sequence.

我们可以用一个输入迭代器来实现这个算法,但是我们必须将迭代器递增 n 次才能到达元素。那是线性时间复杂度,或 O(n)。使用随机访问迭代器,我们可以在常数时间或 O(1) 内执行此操作,如下一个清单所示。

We can implement this algorithm with an input iterator, but we would have to increment the iterator n times to reach the element. That is linear time complexity, or O(n). With a random-access iterator, we can do this in constant time, or O(1), as shown in the next listing.

清单 10.38。元素在
函数 elementAtRandomAccessIterator<T>(
    开始:IRandomAccessIterator<T>,结束:IRandomAccessIterator<T>,
    n: 数字): IRandomAccessIterator<T> {
    begin.move(n);                               1个

    如果(开始。距离(结束)<= 0)返回结束;    2个

    回归开始;                                3 
}
function elementAtRandomAccessIterator<T>(
    begin: IRandomAccessIterator<T>, end: IRandomAccessIterator<T>,
    n: number): IRandomAccessIterator<T> {
    begin.move(n);                               1

    if (begin.distance(end) <= 0) return end;    2

    return begin;                                3
}

  • 1 向前移动 begin n 个元素。
  • 1 Move begin n elements forward.
  • 2 如果等于或大于end,则n大于序列,返回end。
  • 2 If it is equal or larger than end, n is larger than the sequence, so return end.
  • 3 否则,返回指向该元素的迭代器。
  • 3 Otherwise, return an iterator to the element.

随机访问迭代器支持最高效的算法,但很少有数据结构可以提供这样的迭代器。

Random-access iterators enable the most efficient algorithms, but fewer data structures can provide such iterators.

10.4.5。迭代器回顾

10.4.5. Iterator recap

我们已经研究了各种类别的迭代器,以及它们的不同功能如何支持更高效的算法。我们从输入和输出迭代器开始,它们对序列执行一次遍历。输入迭代器允许我们读取值,而输出迭代器允许我们设置值。

We’ve looked at the various categories of iterators and how their different capabilities enable more efficient algorithms. We started with input and output iterators, which perform a one-pass traversal over a sequence. Input iterators allow us to read values, whereas output iterators allow us to set values.

这就是我们需要的算法,例如map()filter()reduce(),它们以线性方式处理它们的输入。大多数编程语言只为这种类型的迭代器提供算法库,包括 Java 和 C#,以及它们的Iterable<T>IEnumerable<T>.

This is all we need for algorithms such as map(), filter(), and reduce(), which process their input in linear fashion. Most programming languages provide algorithm libraries for only this type of iterator, including Java and C#, with their Iterable<T> and IEnumerable<T>.

接下来,我们看到添加读取和写入值以及创建迭代器副本的能力,可以启用其他可以就地修改数据的有用算法。这些新功能由前向迭代器提供。

Next, we saw that adding the ability to both read and write a value, and to create a copy of an iterator, enables other useful algorithms that can modify data in place. These new capabilities were supplied by a forward iterator.

在某些情况下,例如示例reverse(),仅向前移动序列是不够的。我们需要双向移动。既可以前进也可以后退的迭代器称为双向迭代器。

In some cases, such as the reverse() example, moving only forward through a sequence is not enough. We need to move both ways. An iterator that can step both forward and backward is called a bidirectional iterator.

最后,如果某些算法可以跳过一个序列并访问任意位置的项目而无需逐步遍历,则它们的性能会更好。排序算法就是很好的例子;elementAt()我们刚刚看到的简单也是如此。为了支持这样的算法,我们引入了随机访问迭代器,它可以一步移动多个元素。

Finally, some algorithms perform better if they can jump around a sequence and access items at arbitrary locations without needing to traverse step by step. Sorting algorithms are good examples; so is the simple elementAt() that we just saw. To support such algorithms, we introduced the random-access iterator, which can move over multiple elements in one step.

这些想法并不新鲜;C++ 标准库提供了一组使用具有类似功能的迭代器的高效算法。其他语言将自己限制在较小的算法集或效率较低的实现上。

These ideas are not new; the C++ standard library provides a set of efficient algorithms that use iterators with similar capabilities. Other languages limit themselves to a smaller set of algorithms or less-efficient implementations.

您可能已经注意到基于迭代器的算法并不流畅,因为它们将一对迭代器作为输入并返回其中一个void或一个迭代器。C++ 正在从迭代器转向范围。我们不会在本书中深入讨论这个主题,但在较高的层次上,一个范围可以被认为是一对begin/end迭代器。更新算法以将范围作为参数并返回范围为更流畅的 API 奠定了基础,我们可以在其中对范围进行链式操作。在未来的某个时候,基于范围的算法很可能会进入其他语言。使用功能强大的迭代器在任何数据结构上运行高效、就地、通用算法的能力非常有用。

You may have noticed that the iterator-based algorithms were not fluent, as they took a pair of iterators as input and returned either void or an iterator. C++ is moving from iterators to ranges. We won’t cover this topic deeply in this book, but at a high level, a range can be thought of as a pair of begin/end iterators. Updating the algorithms to take ranges as arguments and to return ranges sets the stage for a more fluent API in which we can chain operations on ranges. It is likely that at some point in the future, range-based algorithms will make their way into other languages. The ability to run efficient, in-place, generic algorithms over any data structure with a capable-enough iterator is extremely useful.

10.4.6。练习

10.4.6. Exercises

1个

支持drop()跳过范围的前 n 个元素所需的最小迭代器类别是什么?

  1. InputIterator
  2. ForwardIterator
  3. BidirectionalIterator
  4. RandomAccessIterator

1

What is the minimum iterator category required to support drop() that skips the first n elements of a range?

  1. InputIterator
  2. ForwardIterator
  3. BidirectionalIterator
  4. RandomAccessIterator

2个

支持二进制搜索算法(O(log n))所需的最小迭代器类别是什么?提醒一下,二分查找检查范围的中间元素。如果它大于搜索的值,它将范围分成两半并查看前半部分。如果不是,它会查看范围的后半部分,然后重复。这个想法是搜索空间在每一步减半,所以算法的复杂度是 O(log n)。

  1. InputIterator
  2. ForwardIterator
  3. BidirectionalIterator
  4. RandomAccessIterator

2

What is the minimum iterator category required to support a binary search algorithm (with O(log n))? As a reminder, binary search checks the middle element of a range. If it’s larger than the value searched for, it splits the range in halves and looks at the first half. If not, it looks at the second half of the range and then repeats. The idea is that the search space is halved at each step, so the complexity of the algorithm is O(log n).

  1. InputIterator
  2. ForwardIterator
  3. BidirectionalIterator
  4. RandomAccessIterator

10.5。自适应算法

10.5. Adaptive algorithms

我们对迭代器的要求越多,能够提供它的数据结构就越少。我们看到我们可以在单向链表、双向链表或数组上创建前向迭代器。如果我们想要一个双向迭代器,那么单向链表就不适用了。我们可以在双向链表和数组上获得一个双向迭代器,但不能在单链表上获得。如果我们想要一个随机访问迭代器,我们需要删除双向链表。

The more we ask of an iterator, the fewer the data structures that can supply it. We saw that we can create a forward iterator over a singly linked list, a doubly linked list, or an array. If we want a bidirectional iterator, singly linked lists are out of the picture. We can get a bidirectional iterator over doubly linked lists and arrays but not singly linked lists. If we want a random-access iterator, we need to drop doubly linked lists.

我们希望泛型算法尽可能通用,并且它们需要功能最少但足以支持该算法的迭代器。但正如我们刚刚看到的,算法的低效版本对迭代器的要求并不高。对于某些算法,我们可以提供多个版本:使用功能较弱的迭代器的低效版本和使用功能更强的迭代器的效率更高的版本。

We want generic algorithms to be as general as possible, and they require the least capable iterator that is good enough to support the algorithm. But as we just saw, less efficient versions of an algorithm don’t require that much from their iterators. For some algorithms, we can provide multiple versions: a less-efficient version that works with a less-capable iterator and a more-efficient version that works with a more--capable iterator.

让我们回顾一下我们的elementAt()例子。如果 n 大于序列的长度,则此算法将返回序列中的第 n 个值或序列的末尾。如果我们有一个前向迭代器,我们可以递增它 n 次并返回值。这具有线性或 O(n) 的复杂性,因为随着 n 的增加,我们需要执行更多的步骤。另一方面,如果我们有一个随机访问迭代器,我们可以在常量或 O(1) 时间内检索元素。

Let’s revisit our elementAt() example. This algorithm will return the nth value in a sequence or the end of the sequence if n is larger than the length of the sequence. If we have a forward iterator, we can increment it n times and return the value. This has linear, or O(n) complexity, as we need to perform more steps as n increases. On the other hand, if we have a random-access iterator, we can retrieve the element in constant, or O(1), time.

我们是想提供一种更通用、效率更低的算法,还是提供一种限于更少数据结构的更高效的算法?答案是我们不必选择:我们可以提供算法的两个版本,并且根据我们获得的迭代器类型,我们可以利用最有效的实现。

Do we want to provide a more-general, less-efficient algorithm or a more-efficient algorithm that is limited to fewer data structures? The answer is that we don’t have to choose: we can provide two versions of the algorithm, and depending on the type of iterator we get, we can leverage the most-efficient implementation.

让我们实现一个elementAtForwardIterator()以线性时间检索元素的函数和一个elementAtRandomAccessIterator()以常数时间检索元素的函数,如以下清单所示。

Let’s implement an elementAtForwardIterator() that retrieves the element in linear time and an elementAtRandomAccessIterator() that retrieves the element in constant time, as shown in the following listing.

清单 10.39。elementAt()带有输入和随机访问迭代器
函数 elementAtForwardIterator<T>(
    开始: IForwardIterator<T>,结束: IForwardIterator<T>,
    n: 数字): IForwardIterator<T> {
    while (!begin.equals(end) && n > 0) {
        begin.increment();                                           1
        名词--;                                                         1个
    }

    回归开始;                                                    2个
}

函数 elementAtRandomAccessIterator<T>(                            3
    开始:IRandomAccessIterator<T>,结束:IRandomAccessIterator<T>,
    n: 数字): IRandomAccessIterator<T> {
    begin.move(n);

    如果(开始。距离(结束)<= 0)返回结束;

    回归开始;
}
function elementAtForwardIterator<T>(
    begin: IForwardIterator<T>, end: IForwardIterator<T>,
    n: number): IForwardIterator<T> {
    while (!begin.equals(end) && n > 0) {
        begin.increment();                                           1
        n--;                                                         1
    }

    return begin;                                                    2
}

function elementAtRandomAccessIterator<T>(                           3
    begin: IRandomAccessIterator<T>, end: IRandomAccessIterator<T>,
    n: number): IRandomAccessIterator<T> {
    begin.move(n);

    if (begin.distance(end) <= 0) return end;

    return begin;
}

  • 1 当 n 大于 0 并且我们还没有到达序列的末尾时,将迭代器移动到下一个元素并递减 n。
  • 1 While n is greater than 0 and we haven’t reached the end of the sequence, move the iterator to the next element and decrement n.
  • 2 返回开始。这将是序列的第 n 个元素或末尾。
  • 2 Return begin. This will be either the nth element or the end of the sequence.
  • 3 这是上一节中的 elementAt() 实现。
  • 3 This is the elementAt() implementation from the preceding section.

现在我们可以实现一个elementAt()算法,它根据作为参数接收的迭代器的能力来选择要应用的算法,如清单 10.40所示。请注意,TypeScript 不支持函数重载,因此我们需要使用一个函数来确定迭代器的类型。在其他语言中,例如 C# 和 Java,我们可以简单地提供具有相同名称但采用不同参数的方法。

Now we can implement an elementAt() that picks the algorithm to apply based on the capabilities of the iterators it receives as arguments, as shown in listing 10.40. Note that TypeScript doesn’t support function overloading, so we need to use a function that determines the type of the iterator. In other languages, such as C# and Java, we can simply provide methods that have the same name but take different arguments.

清单 10.40。自适应elementAt()
函数 isRandomAccessIterator<T>(
    iter: IForwardIterator<T>): iter 是 IRandomAccessIterator<T> {
    在 iter 中返回“距离”;                                          1个
}

函数 elementAt<T>(
    开始: IForwardIterator<T>,结束: IForwardIterator<T>,
    n: 数字): IForwardIterator<T> {
    如果(isRandomAccessIterator(开始)&& isRandomAccessIterator(结束)){
        返回 elementAtRandomAccessIterator(开始,结束,n);            2个
    } 别的 {
        返回 elementAtForwardIterator(开始,结束,n);                 3个
    }
}
function isRandomAccessIterator<T>(
    iter: IForwardIterator<T>): iter is IRandomAccessIterator<T> {
    return "distance" in iter;                                          1
}

function elementAt<T>(
    begin: IForwardIterator<T>, end: IForwardIterator<T>,
    n: number): IForwardIterator<T> {
    if (isRandomAccessIterator(begin) && isRandomAccessIterator(end)) {
        return elementAtRandomAccessIterator(begin, end, n);            2
    } else {
        return elementAtForwardIterator(begin, end, n);                 3
    }
}

  • 1 如果 iter 具有距离方法,我们将其视为随机访问迭代器。
  • 1 We consider iter to be a random-access iterator if it has a distance method.
  • 2 如果迭代器是随机访问的,我们调用高效的 elementAtRandomAccessIterator() 函数。
  • 2 If iterators are random-access, we call the efficient elementAtRandomAccessIterator() function.
  • 3 如果不是,我们回退到效率较低的 elementAtForwardIterator() 函数。
  • 3 If not, we fall back to the less-efficient elementAtForwardIterator() function.

一个好的算法与其所拥有的一起工作;它适应具有较低效率实现的功能较弱的迭代器,同时为功能更强大的迭代器启用最有效的实现。

A good algorithm works with what it has; it adapts to a less-capable iterator with a less-efficient implementation while enabling the most-efficient implementation for more-capable iterators.

10.5.1。锻炼

10.5.1. Exercise

1个

Implement nthLast(),一个将迭代器返回到范围的倒数第 n 个元素的函数(如果范围太小,则结束)。如果 n 为 1,我们返回一个指向最后一个元素的迭代器;如果 n 为 2,我们返回指向倒数第二个元素的迭代器,依此类推。如果 n 为 0,我们返回指向范围最后一个元素的结束迭代器。

1

Implement nthLast(), a function that returns an iterator to the nth-last element of a range (or end if the range is too small). If n is 1, we return an iterator pointing to the last element; if n is 2, we return an iterator pointing to the second to last element, and so on. If n is 0, we return the end iterator pointing one past the last element of the range.

2个

提示:我们可以通过ForwardIterator两次传递来实现这一点。第一遍计算范围内的元素。在第二遍中,因为我们知道范围的大小,所以我们知道什么时候停止到最后的 n 项。

2

Hint: we can implement this with a ForwardIterator with two passes. The first pass counts the elements of the range. In the second pass, because we know the size of the range, we know when to stop to be n items from the end.

概括

Summary

  • 通用算法对迭代器进行操作,因此它们可以在不同的数据结构中重复使用。
  • Generic algorithms operate on iterators, so they can be reused across different data structures.
  • 每当您编写循环时,请考虑库算法或算法组合是否可以达到相同的结果。
  • Whenever you write a loop, consider whether a library algorithm or a composition of algorithms can achieve the same result.
  • Fluent API 为链接算法提供了一个很好的接口。
  • Fluent APIs provide a nice interface for chaining algorithms.
  • 类型约束允许算法要求它们所操作的类型具有某些功能。
  • Type constraints allow algorithms to require certain capabilities from the types they operate on.
  • 输入迭代器可以读取值并且可以被推进。我们使用输入迭代器从流中读取数据,例如标准输入。读取一个值后,我们不能再读取;我们只能向前迈进。
  • Input iterators can read values and can be advanced. We read from a stream, like standard input, with an input iterator. After we read a value, we can’t reread; we can only move forward.
  • 输出迭代器可以被写入并且可以被推进。我们使用输出迭代器写入流,如标准输出。在我们写入一个值之后,我们就无法读回它了。
  • Output iterators can be written to and can be advanced. We write to a stream, like standard output, with an output iterator. After we write a value, we can’t read it back.
  • 前向迭代器可以读取值、写入、高级和克隆。链表是可以支持前向迭代器的数据结构的一个很好的例子。我们可以移动到下一个元素并持有对当前元素的多个引用,但我们不能移动到前一个元素,除非我们在最初访问它时保存对它的引用。
  • Forward iterators can read values and be written to, advanced, and cloned. A linked list is a good example of a data structure that can support a forward iterator. We can move to the next element and hold multiple references to the current element, but we can’t move to the previous element unless we save a reference to it when we are initially on it.
  • 双向迭代器具有前向迭代器的所有特性,但也可以向后移动。双向链表是支持双向迭代器的数据结构的一个例子。我们可以根据需要移动到下一个和上一个元素。
  • Bidirectional iterators have all the features of forward iterators but can also move backward. A doubly linked list is an example of a data structure that supports a bidirectional iterator. We can move to both the next and the previous element as needed.
  • 随机访问迭代器可以自由移动到序列中的任何位置。数组是一种支持随机访问迭代器的数据结构。我们可以一步跳转到任何元素。
  • Random-access iterators can freely move to any position in a sequence. An array is a data structure that supports a random-access iterator. We can jump in one step to any element.
  • 大多数主流语言都为输入迭代器提供算法库。
  • Most mainstream languages provide algorithm libraries for input iterators.
  • 更强大的迭代器支持更高效的算法。
  • More-capable iterators enable more-efficient algorithms.
  • 自适应算法提供多种实现:迭代器越强大,算法越高效。
  • Adaptive algorithms provide multiple implementations: the more capable the iterators, the more efficient the algorithm.

第 11 章中,我们将把它提升到下一个抽象级别——更高种类的类型——并解释什么是 monad 以及我们可以用它做什么。

In chapter 11, we’ll step it up to the next level of abstraction—higher kinded types—and explain what a monad is and what we can do with it.

习题答案

Answers to exercises

更好的 map()、filter()、reduce()

Better map(), filter(), reduce()

1个

reduce()使用and 的可能实现filter()

函数 concatenateNonEmpty(iter: Iterable<string>): string {
    返回减少(
        筛选(
            迭代器,
            (值) => 值.长度 > 0),
        "", (str1: string, str2: string) => str1 + str2);
}

1

A possible implementation using reduce() and filter():

function concatenateNonEmpty(iter: Iterable<string>): string {
    return reduce(
        filter(
            iter,
            (value) => value.length > 0),
        "", (str1: string, str2: string) => str1 + str2);
}

2个

map()使用and 的可能实现filter()

function squareOdds(iter: Iterable<number>): IterableIterator<number> {
    返回地图(
        筛选(
            迭代器,
            (值) => 值 % 2 == 1),
        (x) => x * x
        );
}

2

A possible implementation using map() and filter():

function squareOdds(iter: Iterable<number>): IterableIterator<number> {
    return map(
        filter(
            iter,
            (value) => value % 2 == 1),
        (x) => x * x
        );
}

 

 

常用算法

Common algorithms

1个

一个可能的实现:

类 FluentIterable<T> {
    /* ... */

    采取(n:数字):FluentIterable<T> {
        返回新的 FluentIterable (this.takeImpl(n));
    }

    private *takeImpl(n: number): IterableIterator<T> {
        for (this.iter 的常量值) {
            如果 (n-- <= 0) 返回;

            屈服值;
         }
    }
}

1

A possible implementation:

class FluentIterable<T> {
    /* ... */

    take(n: number): FluentIterable<T> {
        return new FluentIterable (this.takeImpl(n));
    }

    private *takeImpl(n: number): IterableIterator<T> {
        for (const value of this.iter) {
            if (n-- <= 0) return;

            yield value;
         }
    }
}

2个

一个可能的实现:

类 FluentIterable<T> {
    /* ... */

    drop(n: number): FluentIterable<T> {
        返回新的 FluentIterable(this.dropImpl(n));
    }

    private *dropImpl(n: number): IterableIterator<T> {
        for (this.iter 的常量值) {
            如果 (n-- > 0) 继续;

            屈服值;
        }
    }
}

2

A possible implementation:

class FluentIterable<T> {
    /* ... */

    drop(n: number): FluentIterable<T> {
        return new FluentIterable(this.dropImpl(n));
    }

    private *dropImpl(n: number): IterableIterator<T> {
        for (const value of this.iter) {
            if (n-- > 0) continue;

            yield value;
        }
    }
}

 

 

约束类型参数

Constraining type parameters

1个

使用通用类型约束来确保的可能解决方案TIComparable

function clamp<T extends IComparable<T>>(value: T, low: T, high: T): T {
    如果(value.compareTo(low)== ComparisonResult.LessThan){
        返回低点;
    }

    如果(value.compareTo(high)== ComparisonResult.GreaterThan){
        回报高;
    }

    返回值;
}

1

A possible solution using a generic type constraint to ensure that T is IComparable:

function clamp<T extends IComparable<T>>(value: T, low: T, high: T): T {
    if (value.compareTo(low) == ComparisonResult.LessThan) {
        return low;
    }

    if (value.compareTo(high) == ComparisonResult.GreaterThan) {
        return high;
    }

    return value;
}

 

 

使用迭代器的高效逆向和其他算法

Efficient reverse and other algorithms using iterators

1个

a—drop()甚至可以用于潜在的无限数据流。能够简单的进阶就足够了。

1

a—drop() can be used even on potentially infinite streams of data. Being able simply to advance is sufficient.

2个

d—二分搜索需要能够在每一步都跳到范围的中间才能有效。双向迭代器仍然必须逐个元素地执行以到达范围的一半,这不会使其成为 O(log n)。(一步一步是 O(n) 或线性的。)

2

d—Binary search needs to be able to jump to the middle of the range at each step to be efficient. A bidirectional iterator would still have to step element by element to reach the half of the range, which would not make it O(log n). (Step by step is O(n) or linear.)

 

 

自适应算法

Adaptive algorithms

1个

如果自适应算法接收到双向迭代器,它将从后面递减,而当它接收到前向迭代器时,将使用两次遍历方法。这是一个可能的实现:

函数 nthLastForwardIterator<T>(
    开始: IForwardIterator<T>,结束: IForwardIterator<T>,n:数字)
    : IForwardIterator<T> {
    让长度:数字= 0;
    让 begin2: IForwardIterator<T> = begin.clone();

    // 判断范围的长度
    while (!begin.equals(end)) {
        begin.increment();
        长度++;
    }

    如果(长度 < n)返回结束;

    让 curr: number = 0;

    // 前进直到当前元素是倒数第n个
    while (!begin2.equals(end) && curr < length - n) {
        begin2.增量();
        当前++;
    }

    返回开始2;
}

函数 nthLastBidirectionalIterator<T>(
    开始:IBidirectionalIterator<T>,结束:IBidirectionalIterator<T>,n:数字)
    : IBidirectionalIterator<T> {
    让 curr: IBidirectionalIterator<T> = end.clone();

    while (n > 0 && !curr.equals(begin)) {
        当前递减();
        n--;
    }

    // 如果我们在递减 n 次之前到达开始,则范围太小
    如果(n > 0)返回结束;

    返回电流;
}

函数是双向迭代器<T>(
    iter: IForwardIterator<T>): iter is IBidirectionalIterator<T> {
    在 iter 中返回“减量”;
}


函数 nthLast<T>(
    开始: IForwardIterator<T>,结束: IForwardIterator<T>,n:数字)
    : IForwardIterator<T> {
    if (isBidirectionalIterator(begin) && isBidirectionalIterator(end)) {
        返回 nthLastBidirectionalIterator(开始,结束,n);
    } 别的 {
        返回 nthLastForwardIterator(开始,结束,n);
    }
}

1

An adaptive algorithm will decrement from the back if it receives bidirectional iterators and use the two-pass approach when it receives forward iterators. Here is a possible implementation:

function nthLastForwardIterator<T>(
    begin: IForwardIterator<T>, end: IForwardIterator<T>, n: number)
    : IForwardIterator<T> {
    let length: number = 0;
    let begin2: IForwardIterator<T> = begin.clone();

    // Determine the length of the range
    while (!begin.equals(end)) {
        begin.increment();
        length++;
    }

    if (length < n) return end;

    let curr: number = 0;

    // Advance until the current element is the nth from the back
    while (!begin2.equals(end) && curr < length - n) {
        begin2.increment();
        curr++;
    }

    return begin2;
}

function nthLastBidirectionalIterator<T>(
    begin: IBidirectionalIterator<T>, end: IBidirectionalIterator<T>, n: number)
    : IBidirectionalIterator<T> {
    let curr: IBidirectionalIterator<T> = end.clone();

    while (n > 0 && !curr.equals(begin)) {
        curr.decrement();
        n--;
    }

    // Range is too small if we reached begin before decrementing n times
    if (n > 0) return end;

    return curr;
}

function isBidirectionalIterator<T>(
    iter: IForwardIterator<T>): iter is IBidirectionalIterator<T> {
    return "decrement" in iter;
}


function nthLast<T>(
    begin: IForwardIterator<T>, end: IForwardIterator<T>, n: number)
    : IForwardIterator<T> {
    if (isBidirectionalIterator(begin) && isBidirectionalIterator(end)) {
        return nthLastBidirectionalIterator(begin, end, n);
    } else {
        return nthLastForwardIterator(begin, end, n);
    }
}

 

 

第 11 章。高等类型及以上

Chapter 11. Higher kinded types and beyond

本章涵盖

This chapter covers

  • 适用map()于各种其他类型
  • Applying map() to various other types
  • 封装错误传播
  • Encapsulating error propagation
  • 了解 monad 及其应用
  • Understanding monads and their applications
  • 寻找进一步研究的资源
  • Finding resources for further study

在整本书中,我们研究了一种非常常见的算法的各种版本,map()第 10 章中,我们了解了迭代器如何提供一种抽象,使我们能够在各种数据结构中重用它。在本章中,我们将看到如何将这个算法扩展到迭代器之外并提供一个更通用的版本。这种强大的算法允许我们混合和匹配泛型类型和函数,并且可以通过提供统一的方式来处理错误来提供帮助。

Throughout the book, we’ve looked at various versions of a very common algorithm, map(), and in chapter 10 we saw how iterators provide an abstraction that allows us to reuse it across various data structures. In this chapter, we’ll see how we can extend this algorithm beyond iterators and provide an even more general version. This powerful algorithm allows us to mix and match generic types and functions, and can help by providing a uniform way to handle errors.

看完几个例子后,我们将为这个广泛适用的函数族(称为仿函数)提供一个定义。我们还将解释什么是更高种类的类型以及它们如何帮助我们定义此类通用函数。我们将研究我们在使用缺乏对更高种类类型支持的语言时遇到的限制。

After we go over a few examples, we’ll provide a definition for this broadly applicable family of functions, known as functors. We’ll also explain what higher kinded types are and how they help us define such generic functions. We’ll look at the limitations we run into with languages that lack support for higher kinded types.

接下来,我们将看看 monad。这个词出现在多个地方,虽然听起来很吓人,但这个概念很简单。我们将解释什么是 monad 并介绍多个应用程序,从更好的错误传播到异步代码和序列展平。

Next, we’ll look at monads. The term shows up in multiple places, and although it might sound intimidating, the concept is straightforward. We’ll explain what a monad is and go over multiple applications, from better error propagation to asynchronous code and sequence flattening.

我们将以一个部分结束,讨论我们在本书中学到的一些主题,以及我们没有涉及的其他几种类型:依赖类型和线性类型。我们不会在这里详细介绍;相反,我们将提供一个快速摘要并列出一些资源,以供您了解更多信息。我们推荐几本书来详细了解这些主题中的每一个,以及为其中一些功能提供支持的编程语言。

We will wrap up with a section that discusses some of the topics we learned about in this book and a couple of other kinds of types we did not cover: dependent types and linear types. We won’t go into details here; rather, we’ll provide a quick summary and list some resources in case you want to learn more. We recommend several books to learn more about each of these topics, as well as programming languages that provide support for some of these features.

11.1. 一张更通用的地图

11.1. An even more general map

第 10 章中,我们将仅适用于数组的第 5 章map()的实现更新为适用于迭代器的通用实现,如清单 11.1所示。我们讨论了迭代器如何抽象数据结构遍历,因此我们的新版本可以将函数应用于任何数据结构中的元素(图 11.1)。 map()

In chapter 10, we updated our map() implementation from chapter 5, which worked only on arrays, to a generic implementation that worked on iterators, shown in listing 11.1. We talked about how iterators abstract data structure traversal, so our new version of map() can apply a function to elements in any data structure (figure 11.1).

图 11.1。map()在一个序列上接受一个迭代器,在本例中是一个圆列表,以及一个转换圆的函数。map()将函数应用于序列中的每个元素,并使用转换后的元素生成一个新序列。

清单 11.1。通用的map()
function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):
    IterableIterator<U> {
    for (iter 的常量值) {
        收益函数(值);
    }
}
function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):
    IterableIterator<U> {
    for (const value of iter) {
        yield func(value);
    }
}

此实现适用于迭代器,但我们也应该能够将表单的函数应用于(item: T) => U其他类型。让我们以我们在第 3 章Optional<T>中定义的类型为例,如下一个清单所示。

This implementation works on iterators, but we should be able to apply a function of the form (item: T) => U to other types too. Let’s take, as an example, the Optional<T> type we defined in chapter 3, shown in the next listing.

清单 11.2。可选类型
可选类 <T> {
    私有值:T | 不明确的;
    私人分配:布尔值;

    构造函数(值?:T){
        如果(值){
            this.value = 值;
            this.assigned = true;
        } 别的 {
            this.value = undefined;
            this.assigned = false;
        }
    }

    有值():布尔值{
        返回this.assigned;
    }

    getValue(): T {
        如果(!this.assigned)抛出错误();

        返回<T>这个值;
    }
}
class Optional<T> {
    private value: T | undefined;
    private assigned: boolean;

    constructor(value?: T) {
        if (value) {
            this.value = value;
            this.assigned = true;
        } else {
            this.value = undefined;
            this.assigned = false;
        }
    }

    hasValue(): boolean {
        return this.assigned;
    }

    getValue(): T {
        if (!this.assigned) throw Error();

        return <T>this.value;
    }
}

能够将函数映射(value: T) => UOptional<T>. 如果 optional 包含 type 的值T,则将函数映射到它上面应该返回一个Optional<U>包含应用该函数的结果的 。另一方面,如果可选不包含值,映射将导致空Optional<U>图 11.2)。

It feels natural to be able to map a function (value: T) => U over an Optional<T>. If the optional contains a value of type T, mapping the function over it should return an Optional<U> containing the result of applying the function. On the other hand, if the optional doesn’t contain a value, mapping would result in an empty Optional<U> (figure 11.2).

图 11.2。将函数映射到可选值。如果可选为空,map()则返回一个空的可选;否则,它将函数应用于值并返回包含结果的可选。

让我们勾勒出一个实现。我们将把这个函数放在一个命名空间中。因为 TypeScript 不支持函数重载,所以要有多个同名的函数,我们需要将它们放在不同的命名空间中,以便编译器可以确定我们正在调用的函数。

Let’s sketch out an implementation. We’ll put this function in a namespace. Because TypeScript doesn’t support function overloading, to have multiple functions with the same name, we need to put them in different namespaces so the compiler can determine the function we are calling.

清单 11.3。选修的map()
命名空间可选{
    导出函数 map<T, U>(                                          1
        可选:可选<T>,功能:(值:T)=> U):可选<U> {
        如果(可选。hasValue()){
            返回新的 Optional<U>(func(optional.getValue()));         2个
        } 别的 {
            返回新的可选<U>();                                  3个
        }
    }
}
namespace Optional {
    export function map<T, U>(                                         1
        optional: Optional<T>, func: (value: T) => U): Optional<U> {
        if (optional.hasValue()) {
            return new Optional<U>(func(optional.getValue()));         2
        } else {
            return new Optional<U>();                                  3
        }
    }
}

  • 1 export 只是使函数在命名空间外可见。
  • 1 export simply makes the function visible outside the namespace.
  • 2 如果可选项有一个值,我们提取它,将它传递给 func(),并使用它的结果来初始化一个 Optional<U>。
  • 2 If the optional has a value, we extract it, pass it to func(), and use its result to initialize an Optional<U>.
  • 3 如果可选为空,我们创建一个新的空 Optional<U>。
  • 3 If the optional is empty, we create a new empty Optional<U>.

我们可以用 TypeScript sum 类型T或做一些非常相似的事情undefined。请记住,Optional<T>是这种类型的 DIY 版本,即使在本机不支持求和类型的语言中也能工作,但 TypeScript 支持。让我们看看如何映射“本地”可选类型T | undefined

We can do something very similar with the TypeScript sum type T or undefined. Remember, Optional<T> is a DIY version of such a type that works even in languages that don’t support sum types natively, but TypeScript does. Let’s see how we can map over a “native” optional type T | undefined.

(value: T) => U如果我们有一个类型的值,则映射一个函数T | undefined应该应用该函数并返回它的结果T,或者undefined如果我们以undefined.

Mapping a function (value: T) => U over T | undefined should apply the function and return its result if we have a value of type T, or return undefined if we start with undefined.

清单 11.4。和式map()
命名空间 SumType {
    导出函数 map<T, U>(
        值:T | 未定义,函数:(值:T)=> U):U | 不明确的 {
        如果(值==未定义){
            返回未定义;
        } 别的 {
            返回函数(值);
        }
    }
}
namespace SumType {
    export function map<T, U>(
        value: T | undefined, func: (value: T) => U): U | undefined {
        if (value == undefined) {
            return undefined;
        } else {
            return func(value);
        }
    }
}

map()这些类型不能被迭代,但为它们存在一个函数仍然有意义。让我们定义另一个简单的泛型类型, ,Box<T>如以下清单所示。这种类型只是包装了一个 type 的值T

These types can’t be iterated over, but it still makes sense for a map() function to exist for them. Let’s define another simple generic type, Box<T>, shown in the following listing. This type simply wraps a value of type T.

清单 11.5。箱型
类框 <T> {
    值:T;        1个

    构造函数(值:T){
        this.value = 值;
    }
}
class Box<T> {
    value: T;        1

    constructor(value: T) {
        this.value = value;
    }
}

  • 1 Box<T> 简单地包装了一个 T 类型的值。
  • 1 Box<T> simply wraps a value of type T.

(value: T) => U我们可以在这种类型上映射一个函数吗?我们可以。正如您可能已经猜到的那样,map()forBox<T>会返回 a :它将从 中取出Box<U>值,对其应用函数,然后将结果放回 a ,如图11.3清单 11.6所示。 TBox<T>Box<U>

Can we map a function (value: T) => U over this type? We can. As you might have guessed, map() for Box<T> would return a Box<U>: it will take the value T out of Box<T>, apply the function to it, and put the result back into a Box<U>, as shown in figure 11.3 and listing 11.6.

图 11.3。将函数映射到Box. map()从 中解压值Box,应用函数,然后将值放回 a 中Box

清单 11.6。Box map()
命名空间框 {
    导出函数 map<T, U>(
        box: Box<T>, func: (value: T) => U): Box<U> {
        返回新的 Box<U>(func(box.value));             1个
    }
}
namespace Box {
    export function map<T, U>(
        box: Box<T>, func: (value: T) => U): Box<U> {
        return new Box<U>(func(box.value));             1
    }
}

  • 1 Box<T> 上的 map() 提取值,对其调用 func(),并将结果放入 Box<U> 中。
  • 1 map() over Box<T> extracts the values, calls func() on it, and puts the result into a Box<U>.

我们可以将函数映射到许多泛型类型上。为什么此功能有用?它很有用map(),因为与迭代器一样,它提供了另一种方法来将存储数据的类型与操作该数据的函数分离。

We can map functions over many generic types. Why is this capability useful? It’s useful because map(), like iterators, provides another way to decouple types that store data from functions that operate on that data.

11.1.1。处理结果或传播错误

11.1.1. Processing results or propagating errors

作为一个具体的例子,我们来看几个处理数值的函数。我们将实现一个简单的square()函数,该函数将数字作为参数并返回它的方块。我们还将实现stringify()一个以数字作为参数并返回其字符串表示形式的函数,如下一个清单所示。

As a concrete example, let’s take a couple of functions that process a numerical value. We’ll implement a simple square(), a function that takes a number as an argument and returns its square. We’ll also implement stringify(), a function that takes a number as an argument and returns its string representation, as shown in the next listing.

清单 11.7。square()stringify()
函数平方(值:数字):数字{
    返回值** 2;
}

函数stringify(值:数字):字符串{
    返回值.toString();
}
function square(value: number): number {
    return value ** 2;
}

function stringify(value: number): string {
    return value.toString();
}

现在假设我们有一个readNumber()函数,它从文件中读取一个数值,如清单 11.8所示。因为我们正在处理输入,所以我们可能会遇到一些问题。例如,如果文件不存在或无法打开怎么办?在这种情况下,read-Number()将返回undefined. 我们不会看这个函数的实现;对于我们的例子来说重要的是它的返回类型。

Now let’s say that we have a readNumber() function, which reads a numeric value from a file, as shown in listing 11.8. Because we are dealing with input, we might run into some problems. What if the file doesn’t exist or can’t be opened, for example? In that case, read-Number() will return undefined. We won’t look at the implementation of this function; the important thing for our example is its return type.

清单 11.8。readNumber()返回类型
函数 readNumber():数字 | 不明确的 {
    /* 省略实现 */
}
function readNumber(): number | undefined {
    /* Implementation omitted */
}

如果我们想读取一个数字并通过square()首先应用它来处理它,然后再应用它stringify(),我们需要确保我们实际上有一个数值而不是undefined。一种可能的实现是将 from 转换number | undefinednumberif在需要的地方使用语句,如下一个清单所示。

If we want to read a number and process it by applying square() to it first and then stringify(), we need to ensure that we actually have a numerical value as opposed to undefined. A possible implementation is to convert from number | undefined to number, using if statements wherever needed, as the next listing shows.

清单 11.9。处理号码
函数过程():字符串| 不明确的 {
    让值:数字| undefined = readNumber();

    如果(值==未定义)返回未定义;      1个

    返回 stringify(square(value));               2 
}
function process(): string | undefined {
    let value: number | undefined = readNumber();

    if (value == undefined) return undefined;      1

    return stringify(square(value));               2
}

  • 1 我们需要检查值是否未定义。在这种情况下,我们立即返回 undefined。
  • 1 We need to check whether value is undefined. In that case, we immediately return undefined.
  • 2 我们处理值并返回结果。
  • 2 We process the value and return the result.

我们有两个对数字进行操作的函数,但是因为我们的输入也可以是undefined,所以我们需要明确地处理这种情况。这并不是特别糟糕,但一般来说,我们的代码分支越少,它就越不复杂。它更容易理解和维护,出现错误的机会也更少。另一种看待这个问题的方法是它process()本身只是传播undefined;它对它没有任何用处。process()如果我们能继续负责就更好了处理并让其他人处理错误情况。我们应该怎么做?我们map()实现了求和类型,如下面的清单所示。

We have two functions that operate on numbers, but because our input can also be undefined, we need to handle that case explicitly. This is not particularly bad, but in general, the less branching our code has, the less complex it is. It is easier to understand and to maintain, and there are fewer opportunities for bugs. Another way to look at this is that process() itself simply propagates undefined; it doesn’t do anything useful with it. It would be better if we could keep process() responsible for processing and let someone else handle error cases. How can we do this? With the map() we implemented for sum types, as shown in the following listing.

清单 11.10。处理与map()
命名空间 SumType {
    导出函数 map<T, U>(                                          1
        值:T | 未定义,函数:(值:T)=> U):U | 不明确的 {
        如果(值==未定义){
            返回未定义;

        } 别的 {
            返回函数(值);
        }
    }
}

函数过程():字符串| 不明确的 {
    让值:数字| undefined = readNumber();

    让平方值:数字| 未定义 =
        SumType.map(value, square);                                    2个

    返回 SumType.map(squaredValue, stringify);                       3 
}
namespace SumType {
    export function map<T, U>(                                         1
        value: T | undefined, func: (value: T) => U): U | undefined {
        if (value == undefined) {
            return undefined;

        } else {
            return func(value);
        }
    }
}

function process(): string | undefined {
    let value: number | undefined = readNumber();

    let squaredValue: number | undefined =
        SumType.map(value, square);                                    2

    return SumType.map(squaredValue, stringify);                       3
}

  • 1这是我们在 清单 11.4中实现的求和类型的 map() 。
  • 1 This is the map() for sum types we implemented in listing 11.4.
  • 2 我们没有显式检查未定义,而是调用 map() 对值应用 square()。如果它是未定义的,map() 将返回给我们未定义的。
  • 2 Instead of explicitly checking for undefined, we call map() to apply square() on the value. If it is undefined, map() will give us back undefined.
  • 3 与 square() 一样,我们将 stringify() 函数映射到 squaredValue 上。如果未定义,map() 将返回未定义。
  • 3 Just as with square(), we map() our stringify() function on the squaredValue. If it is undefined, map() will return undefined.

现在我们的process()实现没有分支。number | undefined解包到 anumber和检查的责任undefinedmap(). map()是通用的,可以跨许多其他类型(例如string | undefined)和许多其他处理功能使用。

Now our process() implementation has no branching. The responsibility for unpacking number | undefined into a number and checking for undefined is handled by map(). map() is generic and can be used across many other types (such as string | undefined) and in many other processing functions.

在我们的例子中,因为square()保证返回一个数字,我们可以创建一个链接square()and的小 lambda stringify(),并将其传递给map()下一个清单。

In our case, because square() is guaranteed to return a number, we can create a small lambda that chains square() and stringify(), and pass that to map() in the next listing.

清单 11.11。使用 lambda 处理
函数过程():字符串| 不明确的 {
    让值:数字| undefined = readNumber();

    返回 SumType.map(值,
        (值: 数字) => stringify(square(value)) );       1 
}
function process(): string | undefined {
    let value: number | undefined = readNumber();

    return SumType.map(value,
        (value: number) => stringify(square(value)));       1
}

  • 1 将 square() 的结果传递给 stringify() 的 Lambda
  • 1 Lambda that passes the result of square() to stringify()

此实现是 的功能实现process(),因为错误传播被委托给map()。我们将在讨论 monad 的 11.2 节中更多地讨论错误处理。现在,让我们看一下map().

This implementation is a functional implementation of process(), in that the error propagation is delegated to map(). We’ll talk more about error handling in section 11.2, which discusses monads. For now, let’s look at another application of map().

11.1.2。混搭功能应用

11.1.2. Mix-and-match function application

如果没有map()函数族,如果我们有一个square()平方 a 的函数number,我们将不得不实现一些额外的逻辑来numbernumber | undefined求和类型中获取 a 。同样,我们必须实现一些额外的逻辑来从 a 中获取值Box<number>并将其打包回 a 中Box-<number>,如以下清单所示。

Without the map() family of functions, if we have a square() function that squares a number, we would have to implement some additional logic to get a number from a number | undefined sum type. Similarly, we would have to implement some additional logic to get a value from a Box<number> and package it back in a Box-<number>, as the following listing shows.

清单 11.12。拆包值square()
函数 squareSumType(值:数字 | 未定义)        1
    : 号码 | 不明确的 {
    如果(值==未定义)返回未定义;

    返回平方(值);
}

function squareBox(box: Box<number>): Box<number> {      2
    返回新框(正方形(box.value));
}
function squareSumType(value: number | undefined)       1
    : number | undefined {
    if (value == undefined) return undefined;

    return square(value);
}

function squareBox(box: Box<number>): Box<number> {     2
    return new Box(square(box.value));
}

  • 1 此函数包装未定义的检查。
  • 1 This function wraps the undefined check.
  • 2 此函数从 Box 中解包值,然后将结果放入另一个 Box 中。
  • 2 This function unpacks the value from Box and then puts the result into another Box.

到目前为止,这还不算太糟糕。但是如果我们想要类似的东西怎么办stringify()?同样,我们最终将编写两个看起来很像前面的函数的函数,如以下代码所示。

So far, this isn’t too bad. But what if we want something similar with stringify()? Again, we’ll end up writing two functions that look a lot like the previous ones, as shown in the following code.

清单 11.13。拆包值stringify()
函数 stringifySumType(值:数字 | 未定义)
    : 串 | 不明确的 {
    如果(值==未定义)返回未定义;

    返回字符串化(值);
}

function stringifyBox(box: Box<number>): Box<string> {
    返回新框(stringify(box.value))
}
function stringifySumType(value: number | undefined)
    : string | undefined {
    if (value == undefined) return undefined;

    return stringify(value);
}

function stringifyBox(box: Box<number>): Box<string> {
    return new Box(stringify(box.value))
}

这开始看起来像重复代码,这从来都不是好事。如果我们有map()可用于number | undefined和 的函数Box,它们将提供抽象以删除重复代码。我们可以在下一个列表中传递 either square()or stringify()to either SumType.map()or to ;Box.map()不需要额外的代码。

This starts to look like duplicate code, which is never good. If we have map() functions available for number | undefined and Box, they provide the abstraction to remove the duplicate code. We can pass either square() or stringify() to either SumType.map() or to Box.map() in the next listing; no additional code is needed.

清单 11.14。使用map()
让 x: 数字 | 未定义= 1;
让 y: Box<number> = new Box(42);

console.log(SumType.map(x, stringify));
console.log(Box.map(y, stringify));

console.log(SumType.map(x, square));
console.log(Box.map(y, square));
let x: number | undefined = 1;
let y: Box<number> = new Box(42);

console.log(SumType.map(x, stringify));
console.log(Box.map(y, stringify));

console.log(SumType.map(x, square));
console.log(Box.map(y, square));

现在让我们定义这一系列map()函数。

Now let’s define this family of map() functions.

11.1.3。函子和更高种类的类型

11.1.3. Functors and higher kinded types

上一节我们讲的是函子。

What we talked about in the preceding section are functors.

函子

仿函数是执行映射操作的函数的概括。对于像 的任何泛型类型Box<T>,从tomap()获取 aBox<T>和一个函数并生成 a 的操作是一个函子(图 11.4)。 TUBox<U>

A functor is a generalization of functions that perform mapping operations. For any generic type like Box<T>, a map() operation that takes a Box<T> and a function from T to U and produces a Box<U> is a functor (figure 11.4).

图 11.4。我们有一个泛型类型H,它包含 0、1 或更多某种类型的值和一个从到 的T函数。在这种情况下,是一个空圆,是一个完整的圆。仿函数从实例中解压or ,应用函数,然后将结果放回. TUTUmap()TTH<T>H<U>

函子是非常强大的概念,但大多数主流语言都没有很好的方式来表达它们,因为函子的一般定义依赖于更高种类的类型。

Functors are extremely powerful concepts, but most mainstream languages do not have a good way to express them because the general definition of a functor relies on higher kinded types.

高等类型

泛型类型是具有类型参数的类型,例如泛型类型T,或类似Box<T>具有类型参数的类型T。高阶类型,就像高阶函数一样,表示一个类型参数和另一个类型参数。T<U>或者Box<T<U>>,例如,有一个类型参数 T ,而 T 又有一个类型参数U

A generic type is a type that has a type parameter, such as a generic type T, or a type like Box<T> that has a type parameter T. A higher kinded type, just like a higher-order function, represents a type parameter with another type parameter. T<U> or Box<T<U>>, for example, have a type parameter T that in turn has a type parameter U.

类型构造函数

在类型系统中,我们可以将类型构造函数视为返回类型的函数。这不是我们自己会实施的东西;这就是类型系统在内部看待类型的方式。

In type systems, we can consider a type constructor to be a function that returns a type. This is not something that we would implement ourselves; this is how the type system looks at types internally.

每种类型都有一个构造函数。一些构造函数是微不足道的。类型的构造函数number可以被认为是一个不带参数并返回类型的函数number。这将是() -> [number type]

Every type has a constructor. Some constructors are trivial. The constructor for the type number can be thought of as a function that takes no arguments and returns the type number. This would be () -> [number type].

square()即使是具有该类型的函数(例如)(value: number) => number也仍然具有不带参数的类型构造函数() -> [(value: number) => number type],因为即使该函数带有参数,但其类型却没有;它总是一样的。

Even a function, such as square(), that has the type (value: number) => number still has a type constructor with no arguments () -> [(value: number) => number type] because even though the function takes an argument, its type doesn’t; it’s always the same.

当我们谈到泛型时,事情会变得更有趣。泛型类型(例如T[])确实需要实际类型参数来生成具体类型。它的类型构造函数是(T) -> [T[] type]. 例如,当Tis 时number,我们得到一个数字数组number[]作为我们的类型,但是当Tis 时string,我们得到一个字符串数组类型string[]。这样的构造函数也称为种类——即类型的种类T[]

Things get more interesting when we get to generics. A generic type, such as T[], does need an actual type parameter to produce a concrete type. Its type constructor is (T) -> [T[] type]. When T is number, for example, we get an array of numbers number[] as our type, but when T is string, we get an array of strings type string[]. Such a constructor is also called a kind—that is, the kind of types T[].

更高种类的类型,如高阶函数,将事物提升了一个层次。在这种情况下,我们的类型构造函数可以将另一个类型构造函数作为参数。让我们以 type 为例T<U>[],它是某种类型的数组T,也有一个 type argument U。我们的第一个类型构造函数接受 aU并生成 a T<U>。我们需要将它传递给T<U>[]从它生成的第二个类型的构造函数((U) -> [T<U> type]) -> [T<U>[] type]

Higher kinded types, like higher-order functions, take things one level up. In this case, our type constructor can take another type constructor as an argument. Let’s take the type T<U>[], which is an array of some type T that also has a type argument U. Our first type constructor takes a U and produces a T<U>. We need to pass this to a second type constructor that produces T<U>[] from it ((U) -> [T<U> type]) -> [T<U>[] type].

正如高阶函数是将其他函数作为参数的函数一样,高阶类型是将其他类型作为参数的类型(参数化类型构造函数)。

Just as higher-order functions are functions that take other functions as argument, higher kinded types are kinds (parameterized type constructors) that take other kinds as arguments.

从理论上讲,我们可以深入到任意数量的级别,例如T<U<V<W>>>,但实际上,在第一T<U>级之后,事情变得不那么有用了。

In theory, we can go any number of levels deep to something like T<U<V<W>>>, but in practice, things become less useful after the first T<U> level.

因为我们没有在 TypeScript、C# 或 Java 中表达更高种类类型的好方法,所以我们无法通过使用类型系统来表达函子来定义构造。Haskell 和 Idris 等具有更强大类型系统的语言使这些定义成为可能。但是,在我们的例子中,因为我们无法通过类型系统强制执行此功能,所以我们可以将其更多地视为一种模式。

Because we don’t have a good way to express higher kinded types in TypeScript, C#, or Java, we can’t define a construct by using the type system to express a functor. Languages such as Haskell and Idris, which have more powerful type systems, make these definitions possible. In our case, though, because we can’t enforce this capability through the type system, we can think of it as more of a pattern.

我们可以说仿函数是具有类型参数T( H<T>) 的任何类型 H ,我们有一个函数map()接受一个类型的参数H<T>和一个函数从Tto U,并返回一个类型的值H<U>

We can say that a functor is any type H with a type parameter T (H<T>) for which we have a function map() that takes an argument of type H<T> and a function from T to U, and returns a value of type H<U>.

或者,如果我们想要更加面向对象,我们可以创建map()一个成员函数,H<T>如果它有一个方法从tomap()获取一个函数并返回一个类型的值,那么它就是一个仿函数。要准确了解类型系统的不足之处,我们可以尝试为它绘制一个接口。让我们调用此接口 并在下一个清单中 声明它。TUH<U>Functormap()

Alternatively, if we want to be more object-oriented, we can make map() a member function and say that H<T> is a functor if it has a method map() that takes a function from T to U and returns a value of type H<U>. To see exactly where the type system is lacking, we can try to sketch out an interface for it. Let’s call this interface Functor and have it declare map() in the next listing.

清单 11.15。Functor接口 示意图
接口函子 <T> {
    map<U>(func: (value: T) => U): Functor<U>;
}
interface Functor<T> {
    map<U>(func: (value: T) => U): Functor<U>;
}

我们可以更新Box<T>以在以下清单中实现此接口。

We can update Box<T> to implement this interface in the following listing.

清单 11.16。实现接口的盒子
类 Box<T> 实现 Functor<T> {
    值:T;

    构造函数(值:T){
        this.value = 值;
    }

    map<U>(func: (value: T) => U): Box<U> {
        返回新框(函数(this.value));
    }
}
class Box<T> implements Functor<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    map<U>(func: (value: T) => U): Box<U> {
        return new Box(func(this.value));
    }
}

这段代码编译;唯一的问题是它不够具体。调用map()onBox<T>返回一个 type 的实例Box<U>。但是如果我们使用Functor接口,我们会看到map()声明指定它返回 a Functor<U>,而不是 a Box<U>。这不够具体。当我们声明接口时,我们需要一种方法来指定 的返回类型map()(在本例中为Box<U>)。

This code compiles; the only problem is that it isn’t specific enough. Calling map() on Box<T> returns an instance of type Box<U>. But if we work with Functor interfaces, we see that the map() declaration specifies that it returns a Functor<U>, not a Box<U>. This isn’t specific enough. We need a way to specify, when we declare the interface, exactly what the return type of map() will be (in this case, Box<U>).

我们希望能够说,“这个接口将由一个H带有类型参数的类型来实现T。” 以下代码显示了如果 TypeScript 支持更高种类的类型,此声明的外观。它显然无法编译。

We would like to be able to say, “This interface will be implemented by a type H with a type argument T.” The following code shows how this declaration would look like if TypeScript supported higher kinded types. It obviously doesn’t compile.

清单 11.17。Functor界面
接口仿函数<H<T>> {
    映射<U>(函数:(值:T)=> U):H<U>;
}

类 Box<T> 实现 Functor<Box<T>> {
    值:T;

    构造函数(值:T){
        this.value = 值;
    }

    map<U>(func: (value: T) => U): Box<U> {
        返回新框(函数(this.value));
    }
}
interface Functor<H<T>> {
    map<U>(func: (value: T) => U): H<U>;
}

class Box<T> implements Functor<Box<T>> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    map<U>(func: (value: T) => U): Box<U> {
        return new Box(func(this.value));
    }
}

缺少这一点,让我们将我们的map()实现视为将函数应用于某些框中的泛型类型或值的模式。

Lacking this, let’s just think of our map() implementations as a pattern for applying functions to generic types or values in some box.

11.1.4。函数的函子

11.1.4. Functors for functions

请注意,我们还有函数之上的函子。给定一个具有任意数量参数并返回 type 值的函数 T,我们可以映射一个接受 aTU在其上生成 a 的函数,最终得到一个接受与原始函数相同的输入并返回 type 值的函数U在本例中是简单的函数组合,如图11.5map()所示。

Note that we also have functors over functions. Given a function with any number of arguments that returns a value of type T, we can map a function that takes a T and produces a U over it, ending up with a function that takes the same inputs as the original function and returns a value of type U. map() in this case is simply function composition as shown in figure 11.5.

图 11.5。将一个函数映射到另一个函数上构成了这两个函数。结果是一个函数,它采用与原始函数相同的参数并返回第二个函数返回类型的值。这两个功能需要兼容;第二个函数必须期望一个与原始函数返回的参数类型相同的参数。

作为示例,让我们使用一个函数,该函数接受两个 type 的参数T并生成一个 type 的值T,并map()在下一个清单中实现其对应的函数。这将返回一个函数,该函数接受两个类型的参数T并返回一个类型的值U

As an example, let’s take a function that takes two arguments of type T and produces a value of type T, and implement its corresponding map() in the next listing. This returns a function that takes two arguments of type T and returns a value of type U.

清单 11.18。功能map()
命名空间函数{
    导出函数 map<T, U>(
        f: (arg1: T, arg2: T) => T, func: (value: T) => U)      1 
        : (arg1: T, arg2: T) => U {                             2
        返回 (arg1: T, arg2: T) => func(f(arg1, arg2));      3个
    }
}
namespace Function {
    export function map<T, U>(
        f: (arg1: T, arg2: T) => T, func: (value: T) => U)     1
        : (arg1: T, arg2: T) => U {                            2
        return (arg1: T, arg2: T) => func(f(arg1, arg2));      3
    }
}

  • 1 map() 采用函数 (T, T) => T 和函数 T => U 来映射它。
  • 1 map() takes a function (T, T) => T, and a function T => U to map over it.
  • 2 map() 返回一个函数 (T, T) => U。
  • 2 map() returns a function (T, T) => U.
  • 3 该实现仅通过在 f() 的结果上调用 func() 来返回一个由 func() 和 f() 组成的 lambda。
  • 3 The implementation simply returns a lambda that composes func() and f() by calling func() on the result of f().

让我们stringify()映射一个add()接受两个数字并返回它们之和的函数。结果是一个函数,它接受两个数字并返回一个string— 将两个数字相加的字符串化结果,如以下清单所示。

Let’s map stringify() over an add() function that takes two numbers and returns their sum. The result is a function that takes two numbers and returns a string—the stringified result of adding the two numbers, as shown in the following listing.

清单 11.19。应用map()一个函数
函数添加(x:数字,y:数字):数字{                     1
    返回 x + y;
}

function stringify(value: number): 字符串 {                      2
    返回值.toString();
}


const 结果:string = Function.map(add, stringify)(40, 2);     3个
function add(x: number, y: number): number {                    1
    return x + y;
}

function stringify(value: number): string {                     2
    return value.toString();
}


const result: string = Function.map(add, stringify)(40, 2);     3

  • 1 add() 只是对其参数求和。
  • 1 add() simply sums its arguments.
  • 2 stringify() 具有与以前相同的实现。
  • 2 stringify() has the same implementation as before.
  • 3 我们将 stringify() 函数映射到 add() 上。然后我们用参数 40 和 2 调用返回的函数。结果是字符串“42”。
  • 3 We map the stringify() function over add(). Then we call the returned functions with the arguments 40 and 2. The result is the string “42”.

在仿函数之后,我们将介绍最后一个构造:monad。

After functors, we’ll cover one final construct: the monad.

11.1.5。锻炼

11.1.5. Exercise

1个

我们有一个IReader<T>定义单个方法的接口,read(): T. 实现一个仿函数,将函数映射(value: T) => U到 an 上IReader<T>并返回IReader<U>.

1

We have an interface IReader<T> that defines a single method, read(): T. Implement a functor that maps a function (value: T) => U over an IReader<T> and returns an IReader<U>.

11.2. 单子

11.2. Monads

您可能听说过monad这个词,因为它最近受到了很多关注。Monad 正在进入主流编程,因此当您看到它时应该知道它。在第 11.1 节的基础上,在本节中,我们将解释什么是 monad 以及它的用途。我们将从几个示例开始,然后查看一般定义。

You have probably heard the term monad, as it’s been getting a lot of attention lately. Monads are making their way into mainstream programming, so you should know one when you see it. Building on top of section 11.1, in this section we will explain what a monad is and how it is useful. We’ll start with a few examples and then look at the general definition.

11.2.1。结果或错误

11.2.1. Result or error

在 11.1 节中,我们有一个readNumber()返回 的函数number | undefined。我们使用仿函数来对处理进行排序square()stringify()因此如果readNumber()返回undefined,则不会发生任何处理,并且undefined通过管道传播。

In section 11.1, we had a readNumber() function that returned number | undefined. We used functors to sequence processing with square() and stringify(), so that if readNumber() returns undefined, no processing happens, and the undefined is propagated through the pipeline.

只要第一个函数——在本例中——readNumber()可以返回错误,这种类型的排序就适用于函子。但是,如果我们想要链接的任何函数可能出错,会发生什么?假设我们要打开一个文件,将其内容作为字符串读取,然后将该字符串反序列化为一个对象,如清单 11.20Cat所示。

This type of sequencing works with functors as long as only the first function—in this case, readNumber()—can return an error. But what happens if any of the functions we want to chain can error out? Let’s say that we want to open a file, read its content as a string, and then deserialize that string into a Cat object, as shown in listing 11.20.

我们有一个openFile()返回 anError或 a 的函数FileHandle。如果文件不存在,如果它被另一个进程锁定,或者如果用户没有打开它的权限,就会发生错误。如果操作成功,我们将返回文件的句柄。

We have an openFile() function that returns an Error or a FileHandle. Errors can occur if the file doesn’t exist, if it is locked by another process, or if the user doesn’t have permission to open it. If the operation succeeds, we get back a handle to the file.

我们有一个readFile()接受 aFileHandle并返回 anError或 a 的函数string。如果无法读取文件,可能会出现错误,这可能是由于太大而无法放入内存。如果可以读取文件,我们会返回一个string.

We have a readFile() function that takes a FileHandle and returns either an Error or a string. Errors can occur if the file can’t be read, perhaps due to being too large to fit in memory. If the file can be read, we get back a string.

最后,deserializeCat()函数接受一个字符串并返回一个Error或一个Cat实例。如果无法将字符串反序列化为Cat对象,可能会出现错误,这可能是由于缺少属性。

Finally, deserializeCat() function takes a string and returns an Error or a Cat instance. Errors can occur if the string can’t be deserialized into a Cat object, perhaps due to missing properties.

所有这些函数都遵循第 3 章中的“返回结果或错误”模式,即从函数返回有效结果或错误,但不能同时返回两者。返回类型将是一个Either<Error, ...>.

All these functions follow the “return result or error” pattern from chapter 3, which suggests returning either a valid result or an error from a function, but not both. The return type will be an Either<Error, ...>.

清单 11.20。返回结果或错误的函数
声明函数 openFile(path: string): Either<Error, FileHandle>;      1个

声明函数 readFile(handle: FileHandle): Either<Error, string>;    2个

声明函数 deserializeCat(
    serializedCat: 字符串): Either<Error, Cat>;                          3个
declare function openFile(path: string): Either<Error, FileHandle>;      1

declare function readFile(handle: FileHandle): Either<Error, string>;    2

declare function deserializeCat(
    serializedCat: string): Either<Error, Cat>;                          3

  • 1 openFile() 返回错误或文件句柄。
  • 1 openFile() returns an Error or a FileHandle.
  • 2 readFile() 返回错误或字符串。
  • 2 readFile() returns an Error or a string.
  • 3 deserializeCat() 返回错误或 Cat。
  • 3 deserializeCat() returns an Error or a Cat.

我们省略了实现,因为它们并不重要。让我们也快速回顾一下下一个清单中 Either3 章的实现。

We are omitting the implementations, as they are not important. Let’s also quickly review the implementation of Either from chapter 3 in the next listing.

清单 11.21。Either类型
类 Either<TLeft, TRight> {
    私有只读值:TLeft | 对;                       1
    个私有只读左:布尔值;                               1个

    私有构造函数(值:TLeft | TRight,左:布尔值){    2
        this.value = 值;
        this.left = 左;
    }

    isLeft(): 布尔值 {
        返回 this.left;
    }

    getLeft(): TLeft {                                             3
        如果(!this.isLeft())抛出新的错误();

        返回 <TLeft>this.value;
    }

    isRight(): 布尔值 {
        返回 !this.left;
    }
    getRight(): TRight {                                           3
        if (this.isRight()) throw new Error();

        返回 <TRight>this.value;
    }

    static makeLeft<TLeft, TRight>(value: TLeft) {                 4
        返回新的 Either<TLeft, TRight>(value, true);
    }

    static makeRight<TLeft, TRight>(value: TRight) {               4
        返回新的 Either<TLeft, TRight>(value, false);
    }
}
class Either<TLeft, TRight> {
    private readonly value: TLeft | TRight;                       1
    private readonly left: boolean;                               1

    private constructor(value: TLeft | TRight, left: boolean) {   2
        this.value = value;
        this.left = left;
    }

    isLeft(): boolean {
        return this.left;
    }

    getLeft(): TLeft {                                            3
        if (!this.isLeft()) throw new Error();

        return <TLeft>this.value;
    }

    isRight(): boolean {
        return !this.left;
    }
    getRight(): TRight {                                          3
        if (this.isRight()) throw new Error();

        return <TRight>this.value;
    }

    static makeLeft<TLeft, TRight>(value: TLeft) {                4
        return new Either<TLeft, TRight>(value, true);
    }

    static makeRight<TLeft, TRight>(value: TRight) {              4
        return new Either<TLeft, TRight>(value, false);
    }
}

  • 1 该类型包装了 TLeft 或 TRight 的值,并使用了一个标志来跟踪该类型。
  • 1 The type wraps a value of either TLeft or TRight and a flag to keep track of that type is used.
  • 2 私有构造函数,因为我们需要确保值和布尔标志同步
  • 2 Private constructor, as we need to make sure that the value and boolean flag are in sync
  • 3 当我们有一个 TRight 时试图得到一个 TLeft,反之亦然,会抛出一个错误。
  • 3 Attempting to get a TLeft when we have a TRight, or vice versa, throws an error.
  • 4 工厂函数调用构造函数并确保布尔标志与值一致。
  • 4 Factory functions call the constructor and ensure that the boolean flag is consistent with the value.

现在让我们在下一个清单中看看我们如何将这些函数链接在一起成为一个readCatFromFile()函数,该函数将文件路径作为参数并返回一个Cat实例,或者Error如果在此过程中出现任何错误。

Now let’s see in the next listing how we could chain these functions together into a readCatFromFile() function that takes a file path as an argument and returns a Cat instance, or an Error if anything went wrong along the way.

清单 11.22。处理并显式检查错误
function readCatFromFile(path: string): Either<Error, Cat> {             1 
    let handle: Either<Error, FileHandle> = openFile(path);             2个

    如果 (handle.isLeft()) 返回 Either.makeLeft(handle.getLeft());      3个

    让内容:Either<Error, string> = readFile(handle.getRight());   4个

    如果 (content.isLeft()) 返回 Either.makeLeft(content.getLeft());    5个

    返回 deserializeCat(content.getRight());                          6 
}
function readCatFromFile(path: string): Either<Error, Cat> {            1
    let handle: Either<Error, FileHandle> = openFile(path);             2

    if (handle.isLeft()) return Either.makeLeft(handle.getLeft());      3

    let content: Either<Error, string> = readFile(handle.getRight());   4

    if (content.isLeft()) return Either.makeLeft(content.getLeft());    5

    return deserializeCat(content.getRight());                          6
}

  • 1 readCatFromFile() 返回错误或 Cat 实例。
  • 1 readCatFromFile() returns either an Error or a Cat instance.
  • 2 首先,我们尝试打开文件。我们返回错误或文件句柄。
  • 2 First, we attempt to open the file. We get back either an Error or a FileHandle.
  • 3 如果出现错误,我们会提前返回。我们调用 Either.makeLeft(),因为我们需要将 Either<Error, FileHandle> 转换为 Either<Error, Cat>。我们从 Either<Error, FileHandle> 中解压错误并将其打包回 Either<Error, Cat>。
  • 3 If we have an Error, we return early. We call Either.makeLeft(), as we need to convert from Either<Error, FileHandle> to Either<Error, Cat>. We unpack the Error from Either<Error, FileHandle> and pack it back into an Either<Error, Cat>.
  • 4 如果我们有一个 FileHandle,我们会尝试读取文件的内容。
  • 4 If we have a FileHandle, we attempt to read the content of the file.
  • 5 同样,如果我们在读取文件时遇到错误,我们会提前返回。
  • 5 Similarly, we return early if we encountered an error reading the file.
  • 6 最后,如果我们有内容,我们调用 deserializeCat()。因为此函数与 readCatFromFile() 具有相同的返回类型,所以我们只返回其结果。
  • 6 Finally, if we have the content, we call deserializeCat(). Because this function has the same return type as readCatFromFile(), we simply return its result.

process()这个函数与本章前面的第一个实现非常相似。在那里,我们提供了一个更新的实现,从函数中删除了所有分支和错误检查,并将这些任务委托给map(). 让我们看看代码清单 11.23map()中的forEither<TLeft, TRight>是什么样子的。我们将遵循“对的就是对的;对的就是对的;left is error”,这意味着它包含一个错误,所以只会传播它。仅当包含 a时才会应用给定的函数。 TLeftmap()map()EitherTRight

This function is very similar to the first implementation of process() earlier in this chapter. There, we provided an updated implementation that removed all the branching and error checking from the function and delegated those tasks to map(). Let’s see what a map() for Either<TLeft, TRight> would look like in listing 11.23. We will follow the convention “Right is right; left is error,” which means that TLeft contains an error, so map() will just propagate it. map() will apply a given function only if the Either contains a TRight.

清单 11.23。Either map()
命名空间要么{
    导出函数 map<TLeft, TRight, URight>(
        值:<TLeft, TRight>,
        func: (value: TRight) => URight): Either<TLeft, URight> {        1 
        if (value.isLeft()) return Either.makeLeft(value.getLeft());    2个

        返回 Either.makeRight(func(value.getRight()));                3个
    }
}
namespace Either {
    export function map<TLeft, TRight, URight>(
        value: Either<TLeft, TRight>,
        func: (value: TRight) => URight): Either<TLeft, URight> {       1
        if (value.isLeft()) return Either.makeLeft(value.getLeft());    2

        return Either.makeRight(func(value.getRight()));                3
    }
}

  • 1 func() 仅在输入 Either 包含 TRight 类型的值时应用,因此其参数必须为 TRight 类型。
  • 1 func() is only applied if the input Either contains a value of type TRight, so its argument must be of type TRight.
  • 2 如果输入包含 TLeft,我们将其从 Either<TLeft, TRight> 中解包,然后将其重新打包为 Either<TLeft, URight>。
  • 2 If the input contains a TLeft, we unpack it from Either<TLeft, TRight> and repack it into Either<TLeft, URight>.
  • 3 如果输入包含 TRight,我们将其解包,对其应用 func(),并将结果打包到 Either<TLeft, URight> 中。
  • 3 If the input contains a TRight, we unpack it, apply func() to it, and pack the result it into an Either<TLeft, URight>.

但是,使用 有一个问题map():它期望作为参数的函数类型与我们正在使用的函数不兼容。使用map(),在我们调用openFile()并取回一个之后Either<Error, FileHandle>,我们需要一个函数(value: FileHandle) => string来读取它的内容。该函数本身不能返回Error, likesquare()stringify()。但在我们的例子中,readFile()可能会失败,所以它不会返回string;它返回Either<Error, string>。如果我们尝试在我们的 中使用它readCatFromFile(),就会出现编译错误,如下一个清单所示。

There is a problem with using map(), though: the types of the functions it expects as arguments are incompatible with the functions we are using. With map(), after we call openFile() and get back an Either<Error, FileHandle>, we would need a function (value: FileHandle) => string to read its content. That function can’t itself return an Error, like square() or stringify(). But in our case, readFile() can fail, so it doesn’t return string; it returns Either<Error, string>. If we attempt to use it in our readCatFromFile(), we get a compilation error, as the next listing shows.

清单 11.24。不兼容的类型
函数 readCatFromFile(path: string): Either<Error, Cat> {
    让 handle: Either<Error, FileHandle> = openFile(path);

    让内容:Either<Error, string> = Either.map(handle, readFile);     1个

    /* ... */
}
function readCatFromFile(path: string): Either<Error, Cat> {
    let handle: Either<Error, FileHandle> = openFile(path);

    let content: Either<Error, string> = Either.map(handle, readFile);     1

    /* ... */
}

  • 1 由于类型不匹配,编译失败。
  • 1 This fails to compile due to a type mismatch.

我们得到的错误信息是

The error message we get is

类型 'Either<Error, Either<Error, string>>' 不是
可分配给类型“Either<Error, string>”。
Type   'Either<Error,   Either<Error,   string>>'   is   not
assignable to type 'Either<Error, string>'.

我们的仿函数在这里不足。仿函数可以通过处理管道传播初始错误,但如果管道中的每个步骤都可能失败,仿函数将不再工作。在图11.6中,黑色方块代表一个Error,白圈和黑圈代表两种类型,比如FileHandlestring

Our functor falls short here. Functors can propagate an initial error through the processing pipeline, but if every step in the pipeline can fail, functors no longer work. In figure 11.6, the black square represents an Error, and the white and black circles represent two types, such as FileHandle and string.

图 11.6。在这种情况下我们不能使用仿函数,因为仿函数被定义为将函数从白色圆圈映射到黑色圆圈。Either不幸的是,我们的函数返回一个已经包含在(an )中的类型Either<black square, black circle>。我们需要一个替代方案来map()处理这种类型的功能。

map()fromEither<Error, FileHandle>需要一个函数 from File-Handletostring来生成一个Either<Error, string>. readFile()另一方面,我们的功能是从FileHandleEither<Error, string>

map() from Either<Error, FileHandle> would need a function from File-Handle to string to produce an Either<Error, string>. Our readFile() function, on the other hand, is from FileHandle to Either<Error, string>.

这个问题很容易解决。我们需要一个类似于map()from Tto的函数Either<Error, U>,如下一个清单所示。这种函数的标准名称是bind().

This problem is easy to fix. We need a function similar to map() that goes from T to Either<Error, U>, as shown in the next listing. The standard name for such a function is bind().

清单 11.25。Either bind()
命名空间要么{
    导出函数绑定<TLeft, TRight, URight>(
        值:<TLeft, TRight>,
        func: (值: TRight) => Either<TLeft, URight>        1
        ): Either<TLeft, URight> {
        如果 (value.isLeft()) 返回 Either.makeLeft(value.getLeft());

        返回 func(value.getRight());                        2个
    }
}
namespace Either {
    export function bind<TLeft, TRight, URight>(
        value: Either<TLeft, TRight>,
        func: (value: TRight) => Either<TLeft, URight>       1
        ): Either<TLeft, URight> {
        if (value.isLeft()) return Either.makeLeft(value.getLeft());

        return func(value.getRight());                       2
    }
}

  • 1 func() 与 map() 中的 func() 具有不同的类型。
  • 1 func() has a different type from the func() in map().
  • 2 我们可以简单地返回 func() 的结果,因为它与 bind() 的结果具有相同的类型。
  • 2 We can simply return the result of func(), as it has the same type as the result of bind().

正如我们所看到的,实现比 for 的实现更简单:在我们解压值之后,我们简单地返回应用它map()的结果。func()让我们在下一个清单中 使用bind()它来实现我们的功能,并获得所需的无分支错误传播行为。readCatFromFile()

As we can see, the implementation is even simpler than the one for map(): after we unpack the value, we simply return the result of applying func() to it. Let’s use bind() to implement our readCatFromFile() function in the next listing and get the desired branchless error propagation behavior.

清单 11.26。无分支readCatFromFile()
函数 readCatFromFile(path: string): Either<Error, Cat> {
    让 handle: Either<Error, FileHandle> = openFile(path)

    让内容:Either<Error, string> =
        Either.bind(handle, readFile);               1个

    返回 Either.bind(content, deserializeCat);     2 
}
function readCatFromFile(path: string): Either<Error, Cat> {
    let handle: Either<Error, FileHandle> = openFile(path)

    let content: Either<Error, string> =
        Either.bind(handle, readFile);               1

    return Either.bind(content, deserializeCat);     2
}

  • 1 与 map() 不同,此代码有效。应用 readFile() 来处理返回一个 Either<Error, string>。
  • 1 Unlike map(), this code works. Applying readFile() to handle gives us back an Either<Error, string>.
  • 2 deserializeCat()和readCatFromFile()的返回类型相同,所以我们简单的返回bind()的结果。
  • 2 deserializeCat() has the same return type as readCatFromFile(), so we simply return the result of bind().

此版本将、和 无缝链接在一起,openFile()因此如果任何函数失败,错误将作为. 同样,分支被封装在实现中,所以我们的处理函数是线性的。 readFile()deserialize-Cat()readCatFromFile()bind()

This version seamlessly chains together openFile(), readFile(), and deserialize-Cat() so that if any of the functions fails, the error gets propagated as the result of readCatFromFile(). Again, branching is encapsulated in the bind() implementation, so our processing function is linear.

11.2.2。map() 和 bind() 之间的区别

11.2.2. Difference between map() and bind()

在继续定义 monad 之前,让我们来看另一个简化的例子并对比map()and bind()。我们将再次使用Box<T>, 一种简单包装 type 值的泛型类型T。尽管这种类型不是特别有用,但它是我们可以拥有的最简单的泛型类型。我们希望专注于如何map()使用bind()类型的值T以及U在某些通用上下文中使用类型的值,例如Box<T>, Box<U>(or T[], U[]; or Optional<T>, Optional<U>; or Either<Error, T>, Either<Error, U>, 等等)。

Before moving on to define monads, let’s take another simplified example and contrast map() and bind(). We’ll again use Box<T>, a generic type that simply wraps a value of type T. Although this type is not particularly useful, it is the simplest generic type we can have. We want to focus on how map() and bind() work with values of types T and U in some generic context, such as Box<T>, Box<U> (or T[], U[]; or Optional<T>, Optional<U>; or Either<Error, T>, Either<Error, U>, and so on).

对于 a Box<T>, functor ( map()) 接受 aBox<T>和一个来自Tto的函数U并返回 a Box<U>。问题是我们有我们的功能直接从T到的场景Box<U>。这是bind()为了什么。从tobind()获取 aBox<T>和一个函数,并返回将函数应用于内部的结果(图11.7)。 TBox<U>TBox<T>

For a Box<T>, a functor (map()) takes a Box<T> and a function from T to U and returns a Box<U>. The problem is that we have scenarios in which our functions are directly from T to Box<U>. This is what bind() is for. bind() takes a Box<T> and a function from T to Box<U> and returns the result of applying the function to the T inside Box<T> (figure 11.7).

图 11.7。对比map()bind()。对 amap()应用函数并返回 a 。对 a应用函数并返回 a 。 T => UBox<T>Box<U>bind()T => Box><U>Box<T>Box<U>

如果我们有一个stringify()接受数字并返回其字符串表示形式的函数,我们可以map()在 a 上执行它Box<number>并返回 a Box<string>,如以下清单所示。

If we have a function stringify() that takes a number and returns its string representation, we can map() it on a Box<number> and get back a Box<string>, as shown in the following listing.

清单 11.27。map()Box
命名空间框 {
    导出函数 map<T, U>(                                1
        box: Box<T>, func: (value: T) => U): Box<U> {
        返回新的 Box<U>(func(box.value));
    }
}

function stringify(value: number): 字符串 {                   2
    返回值.toString();
}

const s: Box<string> = Box.map(new Box(42), stringify);      3个
namespace Box {
    export function map<T, U>(                               1
        box: Box<T>, func: (value: T) => U): Box<U> {
        return new Box<U>(func(box.value));
    }
}

function stringify(value: number): string {                  2
    return value.toString();
}

const s: Box<string> = Box.map(new Box(42), stringify);      3

  • 1 本章前面的 Box 的 map() 实现。
  • 1 map() implementation for Box from earlier in this chapter.
  • 2 本章前面的 stringify() 实现接受一个数字并返回一个字符串。
  • 2 stringify() implementation from earlier in this chapter takes a number and returns a string.
  • 3 我们可以在 Box<number> 上映射 stringify() 并返回一个 Box<string>。
  • 3 We can map stringify() on a Box<number> and get back a Box<string>.

如果不是stringify()numberstring,我们有一个直接从 到 的函数boxify()将不起作用。相反,我们需要,如下一个清单所示…… numberBox<string>map()bind()

If instead of stringify(), which goes from number to string, we have a boxify() function that goes from number directly to Box<string>, map() won’t work. We’ll need bind() instead, as shown in the next listing..

清单 11.28。bind()Box
命名空间框 {
    导出函数绑定<T, U>(
        box: Box<T>, func: (value: T) => Box<U>: Box<U> {     1
        返回函数(框。值);
    }
}

function boxify(value: number): Box<字符串> {                  2
    返回新框(value.toString());
}

const b: Box<string> = Box.bind(new Box(42), boxify);         3个
namespace Box {
    export function bind<T, U>(
        box: Box<T>, func: (value: T) => Box<U>): Box<U> {    1
        return func(box.value);
    }
}

function boxify(value: number): Box<string> {                 2
    return new Box(value.toString());
}

const b: Box<string> = Box.bind(new Box(42), boxify);         3

  • 1 bind() 从 Box 中解压值并在其上调用 func()。
  • 1 bind() unpacks the value from Box and calls func() on it.
  • 2 boxify() 与 stringify() 的不同之处在于它返回的是 Box<string> 而不是字符串。
  • 2 boxify() differs from stringify() in that it returns a Box<string> instead of a string.
  • 3 我们可以在 Box<number> 上绑定 boxify() 并取回 Box<string>。
  • 3 We can bind boxify() on a Box<number> and get back a Box<string>.

map()和的结果bind()仍然是Box<string>. 我们仍然从Box<T>Box<U>; 不同之处在于我们如何到达那里。在这种map()情况下,我们需要一个函数 from Tto U。在这种bind()情况下,我们需要一个函数 from Tto Box<U>

The result of both map() and bind() is still a Box<string>. We still go from Box<T> to Box<U>; the difference is how we get there. In the map() case, we need a function from T to U. In the bind() case, we need a function from T to Box<U>.

11.2.3。单子模式

11.2.3. The monad pattern

bind()一个 monad 由一个更简单的函数组成。这个其他函数接受一个类型T并将其包装到泛型类型中,例如Box<T>T[]Optional<T>Either<Error, T>。这个函数通常称为return()or unit()

A monad consists of bind() and one more, simpler function. This other function takes a type T and wraps it into the generic type, such as Box<T>, T[], Optional<T>, or Either<Error, T>. This function is usually called return() or unit().

monad 允许通用地构建程序,同时封装程序逻辑所需的样板代码。使用 monad,一系列函数调用可以表示为抽象数据管理、控制流或副作用的管道。

A monad allows structuring programs generically while encapsulating away boilerplate code needed by the program logic. With monads, a sequence of function calls can be expressed as a pipeline that abstracts away data management, control flow, or side effects.

让我们看几个 monad 的例子。我们可以从我们的简单Box<T>类型开始,并unit()在下一个清单中添加它来完成 monad。

Let’s look at a few examples of monads. We can start with our simple Box<T> type and add unit() to it in the next listing to complete the monad.

清单 11.29。Box单子
命名空间框 {
    导出函数单元<T>(值:T):框<T> {                1
        返回新框(值);
    }

    导出函数绑定<T, U>(
        box: Box<T>, func: (value: T) => Box<U>: Box<U> {     2
        返回函数(框。值);
    }
}
namespace Box {
    export function unit<T>(value: T): Box<T> {               1
        return new Box(value);
    }

    export function bind<T, U>(
        box: Box<T>, func: (value: T) => Box<U>): Box<U> {    2
        return func(box.value);
    }
}

  • 1 unit() 只是调用 Box 的构造函数将给定值包装到 Box<T> 的实例中。
  • 1 unit() simply calls Box’s constructor to wrap the given value into an instance of Box<T>.
  • 2 bind() 从 Box 中解压值并在其上调用 func()。
  • 2 bind() unpacks the value from Box and calls func() on it.

实现非常简单。让我们看看Optional<T>下面清单中的 monad 函数。

The implementation is very straightforward. Let’s look at the Optional<T> monad functions in the following listing.

清单 11.30。可选的单子
命名空间可选{
    导出函数单元<T>(值:T):可选<T> {         1
        返回新的可选(值);
    }

    导出函数绑定<T, U>(
        可选:可选<T>,
        func: (value: T) => 可选<U>: 可选<U> {
        如果(!optional.hasValue())返回新的Optional();    2个

        返回函数(可选的。getValue());                   3个
    }
}
namespace Optional {
    export function unit<T>(value: T): Optional<T> {        1
        return new Optional(value);
    }

    export function bind<T, U>(
        optional: Optional<T>,
        func: (value: T) => Optional<U>): Optional<U> {
        if (!optional.hasValue()) return new Optional();    2

        return func(optional.getValue());                   3
    }
}

  • 1 unit() 接受类型 T 的值并将其包装到 Optional<T> 中。
  • 1 unit() takes a value of type T and wraps it into an Optional<T>.
  • 2 如果可选项为空,bind() 返回一个空的可选类型 Optional<U>。
  • 2 If the optional is empty, bind() returns an empty optional of type Optional<U>.
  • 3 如果可选项包含一个值,bind() 返回对其调用 func() 的结果。
  • 3 If the optional contains a value, bind() returns the result of calling func() on it.

与函子非常相似,如果编程语言不能表达更高种类的类型,我们就没有指定接口的好方法Monad。相反,让我们将 monad 视为一种模式。

Very much as with functors, if a programming language can’t express higher kinded types, we don’t have a good way to specify a Monad interface. Instead, let’s think of monads as a pattern.

单子模式

monad 是一种泛型类型H<T>,我们有一个类似的函数unit()接受一个类型的值T并返回一个类型的值H<T>,而一个类似的函数bind()接受一个类型的值H<T>和一个来自Tto的函数H<U>,并返回一个类型的值H<U>

A monad is a generic type H<T> for which we have a function like unit() that takes a value of type T and returns a value of type H<T>, and a function like bind() that takes a value of type H<T> and a function from T to H<U>, and returns a value of type H<U>.

请记住,由于大多数语言都使用这种模式,无法指定接口供编译器检查,因此在许多情况下,这两个函数和unit()可能bind()会以不同的名称出现。您可能会听到术语monadic,如monadic error handling,这意味着错误处理遵循 monad 模式。

Bear in mind that because most languages use this pattern, without a way to specify an interface for the compiler to check, in many instances the two functions, unit() and bind(), may show up under different names. You may hear the term monadic, as in monadic error handling, which means that error handling follows the monad pattern.

接下来,我们将看另一个例子。你可能会惊讶地发现这个例子出现在本书前面的 第 6 章中;我们只是还没有一个名字。

Next, we’ll look at another example. You may be surprised to see that this example showed up much earlier in this book, in chapter 6; we just didn’t have a name for it yet.

11.2.4。延续单子

11.2.4. The continuation monad

第 6 章中,我们研究了简化异步代码的方法。我们最终看到了承诺。承诺表示将在未来某个时间发生的计算结果。Promise<T>是类型值的承诺T。我们可以使用函数通过链接承诺来安排异步代码的执行then()

In chapter 6, we looked at ways to simplify asynchronous code. We ended up looking at promises. A promise represents the result of a computation that will happen sometime in the future. Promise<T> is the promise of a value of type T. We can schedule execution of asynchronous code by chaining promises, using the then() function.

假设我们有一个函数可以确定我们在地图上的位置。因为这个函数会与 GPS 一起工作,可能需要更长的时间才能完成,所以我们让它异步。它将返回类型的承诺Promise<Location>。接下来,我们有一个函数,在给定位置的情况下,它会联系拼车服务来为我们提供一个Car,如下一个清单所示。

Let’s say we have a function that determines our location on the map. Because this function will work with the GPS, it may take longer to finish, so we make it asynchronous. It will return a promise of type Promise<Location>. Next, we have a function that, given a location, will contact a ride-sharing service to get us a Car, as the next listing shows.

清单 11.31。链接承诺
声明函数 getLocation(): Promise<Location>;
声明函数 hailRideshare(location: Location): Promise<Car>;

让汽车:Promise<Car> = getLocation().then(hailRideshare);         1个
declare function getLocation(): Promise<Location>;
declare function hailRideshare(location: Location): Promise<Car>;

let car: Promise<Car> = getLocation().then(hailRideshare);         1

  • 1 当 getLocation() 返回时,将使用其结果调用 hailRideshare()。
  • 1 When getLocation() returns, hailRideshare() will be invoked with its result.

在这一点上,这对您来说应该非常熟悉。then()究竟是怎样的Promise<T>法术bind()

This should look very familiar to you at this point. then() is just how Promise<T> spells bind()!

正如我们在第 6 章中看到的,我们还可以通过使用创建一个立即解决的承诺Promise.resolve()。这需要一个值并返回一个包含该值的已解决的承诺,这Promise<T>相当于unit().

As we saw in chapter 6, we can also create an instantly resolved promise by using Promise.resolve(). This takes a value and returns a resolved promise containing that value, which is the Promise<T> equivalent of unit().

事实证明,几乎所有主流编程语言都可用的 API 链式承诺是单子的。它遵循我们在本节中看到的相同模式,但在不同的域中。在处理错误传播时,我们的 monad 封装了检查我们是否有一个可以继续操作的值或是否有一个我们应该传播的错误。通过承诺,monad 封装了调度和恢复执行的复杂性。不过模式是一样的。

It turns out that chaining promises, an API available in virtually all mainstream programming languages, is monadic. It follows the same pattern that we saw in this section, but in a different domain. While dealing with error propagation, our monad encapsulated checking whether we have a value that we can continue operating on or have an error that we should propagate. With promises, the monad encapsulates the intricacies of scheduling and resuming execution. The pattern is the same, though.

11.2.5。列表单子

11.2.5. The list monad

另一个常用的 monad 是列表 monad。让我们看一下基于序列的实现:一个divisors()函数,它接受一个数字n并返回一个包含除 1 和它本身之外的所有除数的数组n,如清单 11.32所示。

Another commonly used monad is the list monad. Let’s look at an implementation over sequences: a divisors() function that takes a number n and returns an array containing all of its divisors except 1 and n itself, as shown in listing 11.32.

这个简单的实现从 2 开始,一直到 n 的一半,然后将它找到的所有除以n无余数的数字相加。有更有效的方法来找到一个数的所有约数,但在这种情况下我们将坚持使用一个简单的算法。

This straightforward implementation starts from 2 and goes up to half of n, and adds all numbers it finds that divide n without a remainder. There are more efficient ways to find all divisors of a number, but we’ll stick to a simple algorithm in this case.

清单 11.32。除数
函数除数(n:数字):数字[] {
    让结果:数字[] = [];

    对于(设 i = 2;i <= n / 2;i++){
        如果(n%我==0){
            结果.推(我);
        }
    }

    返回结果;
}
function divisors(n: number): number[] {
    let result: number[] = [];

    for (let i = 2; i <= n / 2; i++) {
        if (n % i == 0) {
            result.push(i);
        }
    }

    return result;
}

现在假设我们想要获取一个数字数组并返回一个包含它们所有除数的数组。我们不需要担心受骗。实现此目的的一种方法是提供一个函数,该函数采用一组输入数字,应用于divisors()每个输入,并将所有调用的结果连接divisors()到最终结果中,如以下代码所示。

Now let’s say we want to take an array of numbers and return an array containing all their divisors. We don’t need to worry about dupes. One way to do this is to provide a function that takes an array of input numbers, applies divisors() to each of them, and joins the results of all the calls to divisors() into a final result, as shown in the following code.

清单 11.33。所有除数
function allDivisors(ns: number[]): number[] {
    让结果:数字[] = [];

    for (const n of ns) {
        结果 = result.concat(除数(n));
    }

    返回结果;
}
function allDivisors(ns: number[]): number[] {
    let result: number[] = [];

    for (const n of ns) {
        result = result.concat(divisors(n));
    }

    return result;
}

事实证明,这种模式很常见。假设我们有另一个函数 ,anagrams()它生成一个字符串的所有排列并返回一个字符串数组。如果我们想要获得一个字符串数组的所有变位词的集合,我们最终会实现一个非常相似的函数,如下一个清单所示。

It turns out that this pattern is common. Let’s say that we have another function, anagrams(), that generates all permutations of a string and returns an array of strings. If we want to get the set of all anagrams of an array of strings, we would end up implementing a very similar function, as the next listing shows.

清单 11.34。所有字谜
声明函数 anagram(input: string): string[];      1个

函数 allAnagrams(输入:字符串[]):字符串[] {       2
    让结果:string[] = [];

    对于(输入的常量输入){
        结果 = result.concat(字谜(输入));
    }

    返回结果;
}
declare function anagram(input: string): string[];      1

function allAnagrams(inputs: string[]): string[] {      2
    let result: string[] = [];

    for (const input of inputs) {
        result = result.concat(anagram(input));
    }

    return result;
}

  • 1 anagram() 实现省略
  • 1 anagram() implementation omitted
  • 2 allAnagrams() 与 allDivisors() 非常相似。
  • 2 allAnagrams() is very similar to allDivisors().

现在让我们看看我们是否可以在下一个清单中用通用函数替换allDivisors()and 。allAnagrams()此函数将接受一个 s 数组T和一个函数 fromT到一个 s 数组,并返回一个s U数组。U

Now let’s see whether we can replace allDivisors() and allAnagrams() with a generic function in the next listing. This function would take an array of Ts and a function from T to an array of Us, and return an array of Us.

清单 11.35。列表bind()
function bind<T, U>(inputs: T[], func: (value: T) => U[]): U[] {     1
    让结果:U[] = [];

    对于(输入的常量输入){
        结果 = result.concat(函数(输入));                        2个
    }

    返回结果;
}

function allDivisors(ns: number[]): number[] {                       3
    返回绑定(ns,除数);
}

函数 allAnagrams(输入:字符串[]):字符串[] {                   4
    返回绑定(输入,字谜);
}
function bind<T, U>(inputs: T[], func: (value: T) => U[]): U[] {    1
    let result: U[] = [];

    for (const input of inputs) {
        result = result.concat(func(input));                        2
    }

    return result;
}

function allDivisors(ns: number[]): number[] {                      3
    return bind(ns, divisors);
}

function allAnagrams(inputs: string[]): string[] {                  4
    return bind(inputs, anagram);
}

  • 1 bind() 接受一个 Ts 数组,一个函数返回一个 Us 数组给定一个 T,并返回一个 Us 数组。
  • 1 bind() takes an array of Ts, a function that returns an array of Us given a T, and returns an array of Us.
  • 2 我们将 func() 应用于每个输入 T 并连接结果。
  • 2 We apply func() to each input T and concatenate the results.
  • 3 allDivisors() 可以通过将 divisors() 绑定到数字数组来表示。
  • 3 allDivisors() can be expressed by binding divisors() to an array of numbers.
  • 4 allAnagrams() 可以通过将anagram() 绑定到字符串数组来表示。
  • 4 allAnagrams() can be expressed by binding anagram() to an array of strings.

您可能已经猜到了,这是bind()列表 monad 的实现。对于列表,bind()将每次调用给定函数返回的数组展平为单个数组。错误传播 monad 决定是传播错误还是应用函数,continuation monad 包装调度,而列表 monad 将一组结果(列表的列表)组合到一个平面列表中。在这种情况下,框是一系列值(图 11.8)。

As you’ve probably guessed, this is the bind() implementation for the list monad. In the case of lists, bind() flattens the arrays returned by each call of the given function into a single array. While the error-propagating monad decides whether to propagate an error or apply a function and the continuation monad wraps scheduling, the list monad combines a set of results (a list of lists) into a single flat list. In this case, the box is a sequence of values (figure 11.8).

图 11.8。List monad:bind()采用 s 序列T(本例中为白色圆圈)和sT =>的函数序列U(本例中为黑色圆圈)。结果是扁平化的Us 列表(黑色圆圈)。

实现unit()是微不足道的。给定 type 的值T,它返回一个仅包含该值的列表。这个 monad 泛化到所有类型的列表:数组、链表和迭代器范围。

The unit() implementation is trivial. Given a value of type T, it returns a list containing just that value. This monad generalizes to all kinds of lists: arrays, linked lists, and iterator ranges.

范畴论

函子和单子来自范畴论,这是数学的一个分支,处理由对象和这些对象之间的箭头组成的结构。使用这些小构建块,我们可以构建诸如仿函数和单子之类的结构。我们现在不谈它的细节;我们只是说多个领域,比如集合论甚至类型系统,都可以用范畴论来表达。

Functors and monads come from category theory, a branch of mathematics that deals with structures consisting of objects and arrows between these objects. With these small building blocks, we can build up structures such as functors and monads. We won’t go into its details now; we’ll just say that multiple domains, like set theory and even type systems, can be expressed in category theory.

Haskell 是一种从范畴论中汲取了很多灵感的编程语言,因此它的语法和标准库使得表达函子、单子和其他结构等概念变得容易。Haskell 完全支持更高种类的类型。

Haskell is a programming language that took a lot of inspiration from category theory, so its syntax and standard library make it easy to express concepts such as functors, monads, and other structures. Haskell fully supports higher kinded types.

也许是因为范畴论的构建块非常简单,我们一直在讨论的抽象概念适用于如此多的领域。我们刚刚看到 monad 在错误传播、异步代码和序列处理的上下文中很有用。

Perhaps because the building blocks of category theory are so simple, the abstractions we’ve been talking about are applicable across so many domains. We just saw that monads are useful in the context of error propagation, asynchronous code, and sequence processing.

尽管大多数主流语言仍然将 monad 视为模式而不是适当的构造,但它们绝对是在不同上下文中反复出现的有用结构。

Although most mainstream languages still treat monads as patterns instead of proper constructs, they are definitely useful structures that show up over and over in different contexts.

11.2.6。其他单子

11.2.6. Other monads

state monad 和 IO monad 是其他几个常见的 monad,它们在具有纯函数(没有副作用的函数)和不可变数据的函数式编程语言中很流行。我们将只提供这些 monad 的高级概述,但如果您决定学习一种函数式编程语言,例如 Haskell,您很可能会在旅程的早期遇到它们。

A couple of other common monads, which are popular in functional programming languages with pure functions (functions that don’t have side effects) and immutable data, are the state monad and the IO monad. We’ll provide only a high-level overview of these monads, but if you decide to learn a functional programming language such as Haskell, you will likely encounter them early in your journey.

状态 monad 封装了一段状态,它随值一起传递。这个 monad 使我们能够编写纯函数,在给定当前状态的情况下,生成一个值和一个更新的状态。将这些链接在一起bind()允许我们通过管道传播和更新状态,而无需将其显式存储在变量中,从而使纯功能代码能够处理和更新状态。

The state monad encapsulates a piece of state that it passes along with a value. This monad enables us to write pure functions that, given a current state, produce a value and an updated state. Chaining these together with bind() allows us to propagate and update state through a pipeline without explicitly storing it in a variable, enabling purely functional code to process and update state.

IO monad 封装了副作用。它允许我们实现仍然可以读取用户输入或写入文件或终端的纯函数,因为不纯的行为已从函数中移除并包装在 IO monad 中。

The IO monad encapsulates side effects. It allows us to implement pure functions that can still read user input or write to a file or terminal because the impure behavior is removed from the function and wrapped in the IO monad.

如果您有兴趣了解更多信息,第 11.3 节提供了一些供进一步研究的资源。

If you are interested in learning more, section 11.3 provides some resources for further study.

11.2.7。锻炼

11.2.7. Exercise

1个

让我们采用Lazy<T>定义为 的函数类型() => T,一个不带参数并返回类型值的函数T。这是Lazy因为它会产生一个T,但只有在我们要求时才会产生。为这种类型 实施unit()map()和。bind()

1

Let’s take the function type Lazy<T> defined as () => T, a function that takes no arguments and returns a value of type T. It’s Lazy because it produces a T, but only when we ask it to. Implement unit(), map(), and bind() for this type.

11.3. 下一步去哪里?

11.3. Where to next?

我们已经涵盖了很多基础知识,从基本类型和组合,到函数类型、子类型、泛型,以及一小部分更高级的类型。不过,我们只是触及了类型系统世界的皮毛。在这最后一节中,我们将探讨一些您可能有兴趣了解更多的主题,并为每个主题提供一些起点。

We have covered a lot of ground, from primitive types and composition, to function types, subtyping, generics, and a sliver of higher kinded types. Still, we’ve barely scratched the surface of the world of type systems. In this final section, we’ll look at a few topics you may be interested in learning more about and provide some starting points for each one.

11.3.1。函数式编程

11.3.1. Functional programming

函数式编程是一种与面向对象编程截然不同的范例。学习函数式编程语言为您提供了另一种思考代码的方式。解决问题的方法越多,分解和解决问题就越容易。

Functional programming is a very different paradigm from object-oriented programming. Learning a functional programming language gives you another way to think about code. The more ways you have to approach a problem, the easier it is to break it down and solve it.

越来越多来自函数式编程的特性和模式正在进入非函数式语言,这证明了它们的适用性。Lambda 和闭包、不可变数据结构和反应式编程都来自函数世界。

More and more features and patterns from functional programming are making their way into nonfunctional languages, which is a testament to their applicability. Lambdas and closures, immutable data structures, and reactive programming all come from the functional world.

最好的入门方法是选择一种函数式编程语言。我推荐 Haskell 作为入门语言。它具有相当简单的语法和非常强大的类型系统,并且建立在坚实的理论基础之上。一本很好的、易于阅读的关于该主题的介绍性书籍是Learn You a Haskell for Great Good!Miran Lipovaca 着,No Starch 出版社出版。

The best way to get started is to pick up a functional programming language. I recommend Haskell as a starting language. It has a fairly simple syntax and a very powerful type system, and it stands on a solid theoretical foundation. A good, easy-to-read introductory book on the topic is Learn You a Haskell for Great Good! by Miran Lipovaca, published by No Starch Press.

11.3.2。泛型编程

11.3.2. Generic programming

正如我们在前几章中看到的,泛型编程支持极其强大的抽象和代码可重用性。泛型编程随着 C++ 标准模板库及其数据结构和算法的混合匹配集合而流行起来。

As we saw in previous chapters, generic programming enables extremely powerful abstractions and code reusability. Generic programming became popular with the C++ standard template library and its mix-and-match collection of data structures and algorithms.

泛型编程起源于抽象代数。Alexander Stepanov 创造了泛型编程这个术语并实现了原始模板库,他写了两本关于这个主题的书:Elements of Programming(与 Paul McJones 合着)和From Mathematics to Generic Programming(与 Daniel E. Rose 合着),均由Addison-Wesley Professional。

Generic programming has its roots in abstract algebra. Alexander Stepanov, who coined the term generic programming and implemented the original template library, wrote two books on the subject: Elements of Programming (coauthored with Paul McJones) and From Mathematics to Generic Programming (coauthored with Daniel E. Rose), both published by Addison-Wesley Professional.

这两本书都涉及到一些数学知识,但我希望这个事实不会让您气馁。代码的优雅和美丽令人惊叹。潜在的主题是,通过正确的抽象,我们不需要妥协:我们可以拥有简洁、高效、易于阅读和优雅的代码。

Both books leverage some math, but I hope that fact won’t discourage you. The elegance and beauty of the code are astonishing. The underlying theme is that with the right abstractions, we don’t need to compromise: we can have code that is succinct, performant, easy to read, and elegant.

11.3.3。高等类型和范畴论

11.3.3. Higher kinded types and category theory

正如我们前面提到的,函子等结构直接来自范畴论。Bartosz Milewski 的Category Theory for Programmers(自行出版)是对该领域的一个非常易于阅读的介绍。

As we mentioned earlier, constructs such as functors come directly from category theory. Bartosz Milewski’s Category Theory for Programmers (self-published) is a surprisingly easy-to-read introduction to this field.

我们讨论了函子和 monad,但对于更高种类的类型还有很多。可能需要一段时间才能渗透到更主流的语言中,但是如果您想领先一步,Haskell 是一种很好的语言,可以用来掌握这些概念。

We talked about functors and monads, but there is a lot more to higher kinded types. It will probably take a while for things to trickle down to more mainstream languages, but if you want to get ahead of the curve, Haskell is a good language with which to grasp these concepts.

能够指定更高级别的抽象(例如 monad)使我们能够编写更可重用的代码。

Having the ability to specify higher-level abstractions such as monads enables us to write even-more-reusable code.

11.3.4。依赖类型

11.3.4. Dependent types

本书没有足够的篇幅来介绍依赖类型,但如果您想了解强大的类型系统使代码更安全的更多方式,这个主题是另一个不错的话题。

We didn’t have space to cover dependent types in this book, but if you want to know more ways that a powerful type system makes code safer, this topic is another good one.

非常简单地,我们看到了一个类型如何决定一个变量可以取的值。我们还研究了泛型,因为一个类型可以指示另一个类型可以是什么(类型参数)。依赖类型扭转了这种情况:我们有决定类型的值。典型的例子是在类型系统中对列表的长度进行编码。例如,包含两个元素的数字列表最终与包含五个元素的数字列表具有不同的类型。将它们连接起来给了我们另一种类型:一个有七个元素的列表。您可以想象在类型系统中对此类信息进行编码如何保证,例如,我们永远不会索引越界。

Very briefly, we saw how a type can dictate the values that a variable can take. We also looked at generics, in that a type can dictate what another type can be (type parameters). Dependent types flip this situation around: we have values that dictate types. The classic example is encoding the length of a list in the type system. A list of numbers with two elements ends up having a different type from a list of numbers with five elements, for example. Concatenating them gives us another type: a list with seven elements. You can imagine how encoding such information in the type system can guarantee, for example, that we never index out of bounds.

如果您想了解有关依赖类型的更多信息,我推荐Edwin Brady 的Type Driven Development with Idris,Manning 出版。Idris 是一种语法与 Haskell 非常相似的编程语言,但它增加了对依赖类型的支持。

If you want to learn more about dependent types, I recommend Type Driven Development with Idris by Edwin Brady, published by Manning. Idris is a programming language with a syntax very similar to Haskell’s, but it adds support for dependent types.

11.3.5。线性类型

11.3.5. Linear types

第 1 章中,我们简要提到了类型系统和逻辑之间的深层联系。线性逻辑与处理资源的经典逻辑不同。与经典逻辑不同,在经典逻辑中,演绎如果为真,则永远为真,而线性逻辑证明会消耗演绎。

In chapter 1, we briefly mentioned the deep connection between type systems and logic. Linear logic is a different take on classic logic that deals with resources. Unlike classic logic, in which a deduction, if true, is true forever, a linear logic proof consumes deductions.

这在编程语言中有直接应用,其中在类型系统中使用线性类型编码资源使用跟踪。Rust 是一种正在稳步流行的编程语言;它使用线性类型来确保资源安全。它的借用检查器确保资源始终只有一个所有者。如果我们将对象传递给函数,我们就转移了资源的所有权,并且编译器不再允许我们引用资源,直到函数交回资源。这种情况旨在消除并发问题,以及 C 可怕的“释放后使用”和“双重释放”。

This has a direct application in programming languages, in which using linear types in a type system encodes resource use tracking. Rust is a programming language that is steadily gaining in popularity; it uses linear types to ensure resource safety. Its borrow checker ensures that there is always a single owner of a resource. If we pass an object to a function, we transfer ownership of the resource, and the compiler no longer allows us to reference the resource until the function hands back the resource. This situation aims to eliminate concurrency issues, as well as the dreaded “use after free” and “double free” of C.

Rust 是另一种值得学习的好语言,因为它具有强大的通用支持和独特的安全特性。Rust 编程语言书籍可在 Rust 网站上免费获得,并很好地介绍了该语言 ( https://doc.rust-lang.org/book )。

Rust is another good language to learn for its powerful generic support and unique safety features. The Rust Programming Language book is available for free on the Rust website and provides a good introduction to the language (https://doc.rust-lang.org/book).

概括

Summary

  • map()将迭代器推广到其他泛型类型。
  • map() generalizes beyond iterators to other generic types.
  • Functor 将数据拆箱与组合和错误传播中的应用程序封装在一起。
  • Functors encapsulate data unboxing with applications in composition and error propagation.
  • 对于更高种类的类型,我们可以通过使用本身具有类型参数的泛型来表达仿函数等构造。
  • With higher kinded types, we can express constructs such as functors by using generics that themselves have type parameters.
  • Monad 允许我们将返回值的操作链接到Box.
  • Monads allow us to chain operations that return values in a Box.
  • 错误 monad 允许我们将返回结果或失败的操作链接在一起,封装错误传播逻辑。
  • Error monads allow us to chain together operations that return result or failure, encapsulating the error-propagation logic.
  • Promises 是封装调度/异步执行的单子。
  • Promises are monads that encapsulate scheduling/asynchronous execution.
  • 列表 monad 应用一个函数,该函数生成一个序列到一个值序列并返回一个展平的序列。
  • The list monad applies a function that produces a sequence to a sequence of values and returns a flattened sequence.
  • 在不支持更高种类类型的语言中,我们可以将仿函数和单子视为可以应用于各种问题的模式。
  • In languages that don’t support higher kinded types, we can think of functors and monads as being patterns that we can apply to various problems.
  • Haskell 是一种很好的学习语言,可以用来理解函数式编程和更高级的类型。
  • Haskell is a good language to learn for understanding functional programming and higher kinded types.
  • Idris 是了解依赖类型及其应用的好语言。
  • Idris is a good language to learn for understanding dependent types and their applications.
  • Rust 是一种很好的学习语言,可以用来理解线性类型及其应用。我希望你喜欢这本书,学到了一些你可以在工作中使用的东西,并获得了一些新的视角。快乐的、类型安全的编程!
  • Rust is a good language to learn for understanding linear types and their applications. I hope that you enjoyed this book, learned something you can use in your work, and gained some new perspectives. Happy, type-safe programming!

11.4. 习题答案

11.4. Answers to exercises

一张更通用的地图

An even more general map

1个

一个可能的实现使用我们在第 5 章中重述的面向对象的装饰器模式来提供另一种类型实现IReader<U>,它包装了一个IReader<T>并且在read()被调用时将给定函数映射到原始值上:

接口 IReader<T> {
    读取():T;
}

命名空间 IReader {
    类 MappedReader<T, U> 实现 IReader<U> {
        阅读器:IReader<T>;
        函数:(值:T)=> U;

        constructor(reader: IReader<T>, func: (value: T) => U) {
            this.reader = 读者;
            这个.func = func;
        }

        读():你{
            返回 this.func(this.reader.read());
        }
    }

    导出函数 map<T, U>(reader: IReader<T>, func: (value: T) => U)
        : IReader<U> {
        返回新的 MappedReader(reader, func);
    }
}

1

A possible implementation uses the object-oriented decorator pattern we recapped in chapter 5 to provide another type implementing IReader<U> that wraps an IReader<T> and, when read() is called, maps the given function over the original value:

interface IReader<T> {
    read(): T;
}

namespace IReader {
    class MappedReader<T, U> implements IReader<U> {
        reader: IReader<T>;
        func: (value: T) => U;

        constructor(reader: IReader<T>, func: (value: T) => U) {
            this.reader = reader;
            this.func = func;
        }

        read(): U {
            return this.func(this.reader.read());
        }
    }

    export function map<T, U>(reader: IReader<T>, func: (value: T) => U)
        : IReader<U> {
        return new MappedReader(reader, func);
    }
}

 

 

单子

Monads

1个

一个可能的实现如下。map()注意和之间的区别bind()

输入 Lazy<T> = () => T;

命名空间懒惰{
    导出函数单元<T>(值:T):惰性<T> {
        返回()=>值;
    }

    导出函数 map<T, U>(lazy: Lazy<T>, func: (value: T) => U)
        : 懒惰<U> {
        返回()=>函数(懒惰());
    }

    导出函数 bind<T, U>(lazy: Lazy<T>, func: (value: T) =>
懒惰<U>)
    : 懒惰<U> {
        返回函数(惰性());
    }
}

1

A possible implementation follows. Notice the difference between map() and bind().

type Lazy<T> = () => T;

namespace Lazy {
    export function unit<T>(value: T): Lazy<T> {
        return () => value;
    }

    export function map<T, U>(lazy: Lazy<T>, func: (value: T) => U)
        : Lazy<U> {
        return () => func(lazy());
    }

    export function bind<T, U>(lazy: Lazy<T>, func: (value: T) =>
Lazy<U>)
    : Lazy<U> {
        return func(lazy());
    }
}

 

 

附录 A。TypeScript 安装和源代码

Appendix A. TypeScript installation and source code

在线的

Online

对于简单的代码,例如尝试一些没有依赖项的代码示例,您可以使用 https://www.typescriptlang.org/play上的在线 TypeScript playground 。

For simple code, such as trying out some code samples without dependencies, you can use the online TypeScript playground at https://www.typescriptlang.org/play.

当地的

Local

要在本地安装,您首先需要 Node.js 和 npm,即 Node 包管理器。您可以在https://www.npmjs.com/get-npm获取它们。有了这些后,运行npm install -g typescript以安装 TypeScript 编译器。

To install locally, you first need Node.js and npm, the Node Package Manager. You can get them at https://www.npmjs.com/get-npm. When you have those, run npm install -g typescript to install the TypeScript compiler.

您可以通过将单个 TypeScript 文件作为参数传递给 TypeScript 编译器来编译它,例如tsc helloworld.ts. TypeScript 编译成 JavaScript。

You can compile a single TypeScript file by passing it as an argument to the TypeScript compiler, such as tsc helloworld.ts. TypeScript compiles to JavaScript.

对于包含多个文件的项目,tsconfig.json 文件用于配置编译器。从带有 tsconfig.json 文件的目录中不带参数运行tsc将根据配置编译整个项目。

For projects that contain multiple files, a tsconfig.json file is used to configure the compiler. Running tsc with no arguments from a directory with a tsconfig.json file will compile the whole project according to the configuration.

源代码

Source Code

本书中的代码示例可从https://github.com/vladris/programming-with-types获得。每章都在自己单独的目录中,并有自己的 tsconfig.json。

The code samples in this book are available at https://github.com/vladris/programming-with-types. Each chapter is in its own separate directory and has its own tsconfig.json.

代码是使用 TypeScript 3.3 版构建的,目标是 ES6 标准,带有strict设置。

Code was built with version 3.3 of TypeScript, targeting the ES6 standard, with strict settings.

每个示例文件都是独立的,因此运行代码示例所需的所有类型和函数都内联在每个示例文件中。每个示例文件都使用唯一的命名空间来防止命名冲突,因为一些示例提供了相同功能或模式的不同实现。

Each sample file is stand-alone, so all types and functions required to run a code sample are inlined within each sample file. Each sample file uses a unique namespace to prevent naming conflicts, because some examples present different implementations of the same function or pattern.

要运行示例文件,请使用tsc;进行编译 然后用 Node.js 运行编译后的 JavaScript 文件。tsc helloworld.ts例如用 编译后,用 运行node helloworld.js

To run a sample file, compile by using tsc; then run the compiled JavaScript file with Node. After compiling with tsc helloworld.ts, for example, run with node helloworld.js.

DIY

DIY

这本书涵盖了 TypeScript 中变体和其他类型的 DIY 实现。对于这些类型的 C# 和 Java 版本,请查看 Maki 类型库:https://github.com/vladris/maki

The book covers DIY implementations for variant and other types in TypeScript. For C# and Java versions of these types, check out the Maki type library: https://github.com/vladris/maki.

附录 B。类型稿速查表

Appendix B. TypeScript cheat sheet

这个备忘单并不详尽。它涵盖了本书中使用的 TypeScript 语法子集。有关完整的 TypeScript 参考,请参阅http://www.typescriptlang.org/docs

This cheat sheet is not exhaustive. It covers the TypeScript syntax subset used in this book. For a full TypeScript reference, see http://www.typescriptlang.org/docs.

表 B.1。原始类型

类型

Type

描述

Description

布尔值 可以是真也可以是假。
数字 64 位浮点数。
细绳 UTF-16 Unicode 字符串。
空白 用作不返回有意义值的函数的返回类型的类型。
不明确的 只能是未定义的。例如,表示已声明但未初始化的变量。
无效的 只能为空。
目的 表示对象或非原始类型。
未知 可以表示任何值。类型安全,因此它不会隐式转换为另一种类型。
任何 绕过类型检查。类型不安全并自动转换为任何其他类型。
绝不 不能代表任何价值

表 B.2。非原始类型(续)

例子

Example

描述

Description

细绳[] 数组类型在类型名称后用 [] 表示——在本例中,是一个字符串数组。
[数字,字符串] 元组被声明为 [] 中的类型列表——在本例中,是一个数字和一个字符串,例如 [0, "hello"]。
(x: 数字, y: 数字) => 数字; 函数类型声明为 () 中的参数列表,然后是 =>,然后是返回类型。
枚举方向 {

北,

东,

南,

西,

}
枚举是用关键字 enum 声明的。在这种情况下,值可以是文字 North、East、South 或 West 之一。
输入点 {

X:数字,

Y:数字

}
具有类型数字的 X 和 Y 属性的类型。
接口 IExpression {

评估():数字;

}
带有返回数字的 evaluate() 方法的接口。
类 Circle extends Shape

implements IGeometry {

// ...

}
Circle 类扩展了 Shape 基类并实现了 IGeometry 接口。
类型 Shape = Circle | 正方形; 联合类型声明为 | 分隔的类型列表。形状是圆形或方形。
类型 SerializableExpression

= Serializable & Expression;
交集类型被声明为一个 & 分隔的类型列表。SerializableExpression 具有 Serializable 的所有成员和 Expression 的所有成员。
表 B.3。声明

宣言

Declaration

描述

Description

让 x: 数字 = 0; 声明一个名为 x 的变量,类型为 number,初始值为 0。
让x:数字; 声明一个名为 x 的变量类型。使用前必须赋值。
const x: 数字 = 0; 声明一个值为 0 的数字类型常量名称 x。x 不能更改。
function add(x: number, y: number)

: number {

return x + y;

}
声明一个函数 add(),它接受两个参数 x 和 y,类型为数字,并返回一个数字。
(x: 数字, y: 数字) => x + y; 接受两个参数并返回它们之和的 lambda(匿名函数)。
namespace Ns {

export function func(): void {

}

}



Ns.func();
命名空间是用 namespace 关键字声明的。命名空间内的声明必须以 export 为前缀才能在命名空间外可见。
类示例 {

a: number = 0;

私人 b: 数字 = 0;

受保护的 c: 数字 = 0;

只读 d:数字;



构造函数(d:数字){

this.d = d;

}



getD(): number {

返回 this.d;

}

}



让实例:Example

= new Example(5);
默认情况下,所有类成员都是公开的。也可以是受保护的(对派生类可见)和私有的(仅在类内部可见)。属性也可以是只读的,在这种情况下,它们在分配后无法修改。除非属性允许 undefined 作为值,否则它们必须内联或使用构造函数进行初始化。任何类的构造函数都是constructor()。类中对类成员的引用必须始终以 this 为前缀。对象用 new 实例化,调用构造函数。
声明 const Sym:唯一符号; 保证唯一的 Symbol。声明为唯一符号的两个常量永远不会相等。
表 B.4。泛型

例子

Example

描述

Description

function identity<T>(value: T): T {

返回值;

}



让 str: string =

identity<string>("Hello");
泛型函数在参数列表之前的 <> 之间有一个或多个类型参数。identity() 有一个类型参数 T。它接受类型 T 的值并返回它。在 <> 之间指定一个具体类型实例化泛型函数。identity<string>() 是 identity() 函数,其中 T 是字符串。
类 Box<T> {

值:T;



构造函数(值:T){

this.value = value;

}

}



让 x: Box<number> = new Box(0);
泛型类在类名之后的 <> 之间有一个或多个类型参数。Box 具有类型 T 的属性值。在 <> 之间指定具体类型可实例化泛型类。Box<number> 是 Box 类,其中 T 是数字。
类 Expr<T 扩展 IExpression> {

/* ... */

}
泛型约束在泛型类型参数之后声明。在此示例中,T 必须支持 IExpression 接口。
表 B.5。类型转换和类型保护

例子

Example

描述

Description

让 x: 未知 = 0;



让 y: number = <number>x;



输入 Point = {

x: 数字;

y:数字;

}



function isPoint(p: unknown):

p is Point {

return

((<Point>p).x

!== undefined) &&

((<Point>p).y

!== undefined);

}



让 p: 未知 = { x: 10, y: 10 };



if (isPoint(p)) {

// p 是点类型

px -= 10;

}
在值之前的 <> 之间指定类型会将值重新解释为该类型。只有在明确地重新解释为数字后,才能将 x 分配给 y。类型谓词是一个布尔值,表示变量属于特定类型。如果我们将 p 重新解释为一个点,并且它同时具有 x 和 ay 成员(均未定义),则 p 是一个点。在类型谓词为真的 if 语句中,测试值会自动重新解释为具有该类型。

类型和可能的值

Types and possible values

名称[部分]

Name [Section]

类型稿类型

TypeScript type

可能的值

Possible values

空类型 [2.1.1] 绝不 没有可能的值
单元类型 [2.1.2] 空白 一个可能的值
求和类型 [3.4.2] 编号 | 细绳 来自数字的值或来自字符串的值
元组(产品类型)[3.1.1] [数字,字符串] 来自数字的值和来自字符串的值
记录(产品类型)[3.1.2] { 一个号码; b:字符串;} 来自数字的(命名)值和来自字符串的(命名)值
函数类型[5.1.2] (值:数字)=> 字符串 函数编号 → 字符串
顶部类型[7.2.1] 未知 任何类型的值
底部类型[7.2.2] 绝不 没有可能的值(底部类型是任何其他类型的子类型)
界面[8.1] 接口 ILogger { /* ... */ } 实现 ILogger 接口的类型的对象
班级[8.2.1] 广场类 { /* ... */ } 方形对象
交叉路口类型[8.4.3] 方形和可记录 具有 Square 和 Loggable 成员的对象
通用类 [9.2.1] 类列表<T> { /* ... */ } 具有类型参数 T 的泛型类 List
通用函数 [9.1.1] 输入 Func<T, U> = (arg: T) => U; 来自 T → U 的函数,其中 T 和 U 是类型参数

常用算法

Common algorithms

地图()

map()

map()将函数应用于范围的每个值并返回应用该函数的结果。

map() applies a function to each value of a range and returns the results of applying that function.

地图([“苹果”,“橙子”,“桃子”],(项目)=> item.length)
map(["apple", "orange", "peach"], (item) => item.length)

筛选()

filter()

filter()将谓词应用于范围的每个值并过滤掉谓词为假的值。

filter() applies a predicate to each value of a range and filters out the values for which the predicate is false.

filter(["苹果", "橙子", "桃子"], (item) => item.length == 5)
filter(["apple", "orange", "peach"], (item) => item.length == 5)

减少()

reduce()

reduce()使用给定函数组合范围内的值并返回单个值。

reduce() combines the values of a range using a given function and returns a single value.

reduce(["苹果", "橙子", "桃子"], "", (acc, item) => acc + item)
reduce(["apple", "orange", "peach"], "", (acc, item) => acc + item)

指数

Index

[符号][ A ][ B ][ C ][ D ][ E ][ F ][ G ][ H ][ I ][ J ][ K ][ L ][ M ][ N ][ O ][ P ][ Q ][ R ][ S ][ T ][ U ][ V ][ W ][ Y ][ Z ]

[SYMBOL][A][B][C][D][E][F][G][H][I][J][K][L][M][N][O][P][Q][R][S][T][U][V][W][Y][Z]

象征

SYMBOL

! character

&& characters

| type operator2nd3rd

|| characters

32-bit integer

4-bit signed integer encoding

4-bit unsigned integer

8-bit unsigned integer

! character

&& characters

| type operator2nd3rd

|| characters

32-bit integer

4-bit signed integer encoding

4-bit unsigned integer

8-bit unsigned integer

A

A

abstract class

accumulate() function

actions

adapter pattern

adaptive algorithms

ADTs (algebraic data types)2nd3rd

  product types

  sum types

Aggregate() function

aggregate() function

algebraic data types.

    See ADTs (algebraic data types).

all() function2nd

AND operator

anonymous function

anonymous functions

anonymous functions (lambdas)

any keyword

Any type

any type2nd3rd4th

any() function2nd

arithmetic overflow

arrays2nd3rd

  associative arrays

  binary trees

  fixed-size arrays

  implementation trade-offs

  list efficiency

associative arrays

associativity

asterisks

async/await function

asynchronous code, simplifying

  async/await function

  promises

    chaining

    chaining synchronous functions

    creating, 2nd

    handling errors



asynchronous execution

  callbacks

  models for

abstract class

accumulate() function

actions

adapter pattern

adaptive algorithms

ADTs (algebraic data types)2nd3rd

  product types

  sum types

Aggregate() function

aggregate() function

algebraic data types.

    See ADTs (algebraic data types).

all() function2nd

AND operator

anonymous function

anonymous functions

anonymous functions (lambdas)

any keyword

Any type

any type2nd3rd4th

any() function2nd

arithmetic overflow

arrays2nd3rd

  associative arrays

  binary trees

  fixed-size arrays

  implementation trade-offs

  list efficiency

associative arrays

associativity

asterisks

async/await function

asynchronous code, simplifying

  async/await function

  promises

    chaining

    chaining synchronous functions

    creating, 2nd

    handling errors



asynchronous execution

  callbacks

  models for

B

bad state

begin iterator2nd3rd4th5th

biased exponent

bidirectional iterators2nd

Big O notation

BigInt type

binary trees

binary64 encoding

bind() function2nd

bit widths

bivariance

bivariant types

Boolean expressions

Boolean types2nd3rd4th5th

  Boolean expressions

  short circuit evaluation

bottom type

bad state

begin iterator2nd3rd4th5th

biased exponent

bidirectional iterators2nd

Big O notation

BigInt type

binary trees

binary64 encoding

bind() function2nd

bit widths

bivariance

bivariant types

Boolean expressions

Boolean types2nd3rd4th5th

  Boolean expressions

  short circuit evaluation

bottom type

C

C

callbacks

catch() function2nd

category theory2nd

clone() function

closed type

closures

code points

coding against interfaces

coercion

collections, subtyping and

composability

composition2nd3rd

  algebraic data types

    product types

    sum types

  composite classes

  compound types

    assigning meaning

    maintaining invariants

    tuples

  either-or types

    enumerations

    optional types

    result or error

    variants

  extending behavior with

  has-a rule of thumb

  implementing

  visitor design pattern

    alternative implementation of

    naïve implementation of

    variant visitor function

compound types2nd

  assigning meaning

  maintaining invariants

  tuples

conditional branching

const notation

constant space (O(1))

constant time (O(1))

constness property



constraints

  enforcing

    with constructor

    with factory

  type parameter constraints

    generic algorithms with

    generic data structures with

continuation monad

continuations

contracts

contracts (interfaces)

contravariant type2nd

correctness

counters

  functional counters

  implementing

  object-oriented counters

  resumable counters

covariant types2nd

cross-cutting concerns

currency addition function

Curry-Howard correspondence

callbacks

catch() function2nd

category theory2nd

clone() function

closed type

closures

code points

coding against interfaces

coercion

collections, subtyping and

composability

composition2nd3rd

  algebraic data types

    product types

    sum types

  composite classes

  compound types

    assigning meaning

    maintaining invariants

    tuples

  either-or types

    enumerations

    optional types

    result or error

    variants

  extending behavior with

  has-a rule of thumb

  implementing

  visitor design pattern

    alternative implementation of

    naïve implementation of

    variant visitor function

compound types2nd

  assigning meaning

  maintaining invariants

  tuples

conditional branching

const notation

constant space (O(1))

constant time (O(1))

constness property



constraints

  enforcing

    with constructor

    with factory

  type parameter constraints

    generic algorithms with

    generic data structures with

continuation monad

continuations

contracts

contracts (interfaces)

contravariant type2nd

correctness

counters

  functional counters

  implementing

  object-oriented counters

  resumable counters

covariant types2nd

cross-cutting concerns

currency addition function

Curry-Howard correspondence

D

data structures, defined

declarations

decorator pattern

  closures

  functional decorator

  implementing

decoupling independent concerns

  generic types

  optional types

  reusable identity functions

dependent types

deserialization



design patterns

  adapter pattern

  decorator pattern

  strategy pattern

  visitor pattern

diamond inheritance problem

dictionaries

downcasts

drop() function

dynamic typing

data structures, defined

declarations

decorator pattern

  closures

  functional decorator

  implementing

decoupling independent concerns

  generic types

  optional types

  reusable identity functions

dependent types

deserialization



design patterns

  adapter pattern

  decorator pattern

  strategy pattern

  visitor pattern

diamond inheritance problem

dictionaries

downcasts

drop() function

dynamic typing

E

eager evaluation2nd

Either type2nd3rd4th5th6th

either value or error

either-or types2nd

  enumerations

  optional types

  result or error

    exceptions

  variants

empty types2nd

encapsulation2nd

encoding libraries

encodings

  UTF-16

  UTF-32

  UTF-8

end iterator2nd3rd4th5th

enum keyword2nd



error cases

  higher kinded types

  promises

  values for

error codes

errors2nd3rd4th

event loops

explicit type cast2nd

exponents

extend() method

extending behavior

  with composition

  with mix-ins

extends keyword

eager evaluation2nd

Either type2nd3rd4th5th6th

either value or error

either-or types2nd

  enumerations

  optional types

  result or error

    exceptions

  variants

empty types2nd

encapsulation2nd

encoding libraries

encodings

  UTF-16

  UTF-32

  UTF-8

end iterator2nd3rd4th5th

enum keyword2nd



error cases

  higher kinded types

  promises

  values for

error codes

errors2nd3rd4th

event loops

explicit type cast2nd

exponents

extend() method

extending behavior

  with composition

  with mix-ins

extends keyword

F

F

filter function

  filter/reduce pipeline

  generic versions of

filter() function2nd3rd4th5th6th

final keyword

find() function2nd3rd

first-class functions

first-order function

first() function

fixed-size arrays2nd3rd

floating-point numbers

floating-point types

  comparing floating-point numbers

  precision values

fmap() function

fold() function2nd

forward iterators

function argument types, subtyping and

function keyword

function map

function return types, subtyping and

function types2nd

  adapter pattern

  counters

    functional counters

    implementing

    object-oriented counters

    resumable counters

  decorator pattern

    closures

    functional decorator

    implementing

  functional programming

  higher-order functions

    filter

    library support

    map

    reduce

  lazy values

    lambdas

  long-running operations

    asynchronous execution

    synchronous execution

  simplifying asynchronous code

    async/await

    promises

  state machines without switch statements

    early programming with types

    implementing

    overview

  strategy pattern

    first-class functions

    implementing

    typing functions

functional counters

functional programming2nd3rd

functors

filter function

  filter/reduce pipeline

  generic versions of

filter() function2nd3rd4th5th6th

final keyword

find() function2nd3rd

first-class functions

first-order function

first() function

fixed-size arrays2nd3rd

floating-point numbers

floating-point types

  comparing floating-point numbers

  precision values

fmap() function

fold() function2nd

forward iterators

function argument types, subtyping and

function keyword

function map

function return types, subtyping and

function types2nd

  adapter pattern

  counters

    functional counters

    implementing

    object-oriented counters

    resumable counters

  decorator pattern

    closures

    functional decorator

    implementing

  functional programming

  higher-order functions

    filter

    library support

    map

    reduce

  lazy values

    lambdas

  long-running operations

    asynchronous execution

    synchronous execution

  simplifying asynchronous code

    async/await

    promises

  state machines without switch statements

    early programming with types

    implementing

    overview

  strategy pattern

    first-class functions

    implementing

    typing functions

functional counters

functional programming2nd3rd

functors

G

G

generic algorithms

  adaptive algorithms

  common algorithms

  higher-order functions

    filter

    filter/reduce pipeline

    map

    reduce

  implementing fluent pipeline

  loops vs.

  type parameter constraints

    generic algorithms with type constraints

    generic data structures with type constraints

  using iterators

generic data structures

  data structures, defined

  decoupling independent concerns

    generic types

    optional types

    reusable identity function

  overview

  streaming data

    processing pipelines

  traversing data structures

    streamlining iteration code

    using iterators

  with type parameter constraints

generic function

generic programming2nd

generic types

generics

glyphs

grapheme-splitter library

graphemes

generic algorithms

  adaptive algorithms

  common algorithms

  higher-order functions

    filter

    filter/reduce pipeline

    map

    reduce

  implementing fluent pipeline

  loops vs.

  type parameter constraints

    generic algorithms with type constraints

    generic data structures with type constraints

  using iterators

generic data structures

  data structures, defined

  decoupling independent concerns

    generic types

    optional types

    reusable identity function

  overview

  streaming data

    processing pipelines

  traversing data structures

    streamlining iteration code

    using iterators

  with type parameter constraints

generic function

generic programming2nd

generic types

generics

glyphs

grapheme-splitter library

graphemes

H

H

has-a rule of thumb

hash function

hash maps

hash tables

Haskell language2nd

heaps

heterogenous collections

  base type or interface

  sum type or variant

  unknown type

higher kinded types

  category theory and

  dependent types

  functional programming

  generic programming

  linear types

  map

    functors

    mix-and-match function application

    processing results or propagating errors

  monads

    common monads

    continuation monad

    list monad

    map vs. bind, 2nd

    monad pattern

    result or error

higher-kinded types2nd3rd

higher-order functions2nd

  filter

    filter/reduce pipeline

    generic versions of

  library support

  map2nd

    bind vs.

    functors

    generic versions of

    mix-and-match function application

    processing results or propagating errors

  reduce

    filter/reduce pipeline

    generic versions of

homogenous collection

has-a rule of thumb

hash function

hash maps

hash tables

Haskell language2nd

heaps

heterogenous collections

  base type or interface

  sum type or variant

  unknown type

higher kinded types

  category theory and

  dependent types

  functional programming

  generic programming

  linear types

  map

    functors

    mix-and-match function application

    processing results or propagating errors

  monads

    common monads

    continuation monad

    list monad

    map vs. bind, 2nd

    monad pattern

    result or error

higher-kinded types2nd3rd

higher-order functions2nd

  filter

    filter/reduce pipeline

    generic versions of

  library support

  map2nd

    bind vs.

    functors

    generic versions of

    mix-and-match function application

    processing results or propagating errors

  reduce

    filter/reduce pipeline

    generic versions of

homogenous collection

I

identities

identity() function2nd3rd

IEEE 7542nd

IForwardIteratorinterface2nd

IIncrementable

IInputIteratorinterface

immutability

implicit type cast

inheritance

  is-a rule of thumb

  modeling hierarchies

  parameterizing behavior

input iterators

instance of keyword

integer types

  overflow and underflow

interfaces (contracts)

intersection types

invariant types2nd

invariants

IOutputIterator

IRandomAccessIterator

IReadable

is keyword

is-a rule of thumb

Iterable argument2nd3rd

Iterable interface2nd3rd4th

IterableIterator interface2nd3rd4th5th6th

iteration

Iterator interface2nd3rd

IteratorResult type2nd3rd



iterators

  algorithms using

    bidirectional iterators

    forward iterators

    iterator building blocks

    random-access iterators

  defined

  streamlining iteration code

  traversing data structures using

IWritable interface2nd

identities

identity() function2nd3rd

IEEE 7542nd

IForwardIteratorinterface2nd

IIncrementable

IInputIteratorinterface

immutability

implicit type cast

inheritance

  is-a rule of thumb

  modeling hierarchies

  parameterizing behavior

input iterators

instance of keyword

integer types

  overflow and underflow

interfaces (contracts)

intersection types

invariant types2nd

invariants

IOutputIterator

IRandomAccessIterator

IReadable

is keyword

is-a rule of thumb

Iterable argument2nd3rd

Iterable interface2nd3rd4th

IterableIterator interface2nd3rd4th5th6th

iteration

Iterator interface2nd3rd

IteratorResult type2nd3rd



iterators

  algorithms using

    bidirectional iterators

    forward iterators

    iterator building blocks

    random-access iterators

  defined

  streamlining iteration code

  traversing data structures using

IWritable interface2nd

J

java.util.stream package

JSON.parse() method2nd3rd

JSON.stringify() method

java.util.stream package

JSON.parse() method2nd3rd

JSON.stringify() method

K

kind property2nd

kind property2nd

大号

L

lambdas (anonymous functions)

lazy evaluation2nd

lazy values

linear space (O(n))

linear time (O(n))

linear types

linearithmic (O(n log n))

linked lists

LinkedList

Liskov substitution principle

list efficiency

list monad

long-running operations

  asynchronous execution

    callbacks

    models for

  synchronous execution

loops, algorithms vs.

lambdas (anonymous functions)

lazy evaluation2nd

lazy values

linear space (O(n))

linear time (O(n))

linear types

linearithmic (O(n log n))

linked lists

LinkedList

Liskov substitution principle

list efficiency

list monad

long-running operations

  asynchronous execution

    callbacks

    models for

  synchronous execution

loops, algorithms vs.

M

machine epsilon

mantissa

map() function2nd3rd4th5th6th7th8th9th10th11th12th

  bind vs.

  functors

  generic versions of

  mix-and-match function application

  processing results or propagating errors

max() function

maybe type

maybe types (optional types)2nd3rd

mix-ins

monadic error handling

monads

  common monads

  continuation monad

  list monad

  map vs. bind

  monad pattern

  result or error

monoids

machine epsilon

mantissa

map() function2nd3rd4th5th6th7th8th9th10th11th12th

  bind vs.

  functors

  generic versions of

  mix-and-match function application

  processing results or propagating errors

max() function

maybe type

maybe types (optional types)2nd3rd

mix-ins

monadic error handling

monads

  common monads

  continuation monad

  list monad

  map vs. bind

  monad pattern

  result or error

monoids

N

N-bit unsigned integer

name property2nd

namespace keyword

NaN (not a number)

narrowing casts

negative infinity

never type2nd3rd4th5th

next elements

next() method2nd3rd4th5th

nominal subtyping

  pros and cons of

  simulating

none() function

nonprimitive types

nonterminating functions

NOT operator

null type2nd3rd

nullable type

number type2nd3rd4th5th6th7th

Number.isSafeInteger() function

numerical types

  arbitrarily large numbers

  floating-point types

    comparing floating-point numbers

    precision values

  integer types

    overflow and underflow

N-bit unsigned integer

name property2nd

namespace keyword

NaN (not a number)

narrowing casts

negative infinity

never type2nd3rd4th5th

next elements

next() method2nd3rd4th5th

nominal subtyping

  pros and cons of

  simulating

none() function

nonprimitive types

nonterminating functions

NOT operator

null type2nd3rd

nullable type

number type2nd3rd4th5th6th7th

Number.isSafeInteger() function

numerical types

  arbitrarily large numbers

  floating-point types

    comparing floating-point numbers

    precision values

  integer types

    overflow and underflow

O

object type2nd3rd4th

object-oriented counters

OOP (object-oriented programming)

  alternatives to

    functional programming

    generic programming

    sum types

  composition

    composite classes

    has-a rule of thumb

    implementing

  extending behavior

    with composition

    with mix-ins

  inheritance

    is-a rule of thumb

    modeling hierarchies

    parameterizing behavior

  interfaces

Optional class

optional type2nd

optional types (maybe types)2nd

Optionaltype2nd3rd4th

OR operator

out keyword

out parameters

overflow

  detecting

  overview

object type2nd3rd4th

object-oriented counters

OOP (object-oriented programming)

  alternatives to

    functional programming

    generic programming

    sum types

  composition

    composite classes

    has-a rule of thumb

    implementing

  extending behavior

    with composition

    with mix-ins

  inheritance

    is-a rule of thumb

    modeling hierarchies

    parameterizing behavior

  interfaces

Optional class

optional type2nd

optional types (maybe types)2nd

Optionaltype2nd3rd4th

OR operator

out keyword

out parameters

overflow

  detecting

  overview

P

P

Pair type

parameterizing behavior

positive infinity

predicates2nd3rd

primitive obsession antipattern

primitive types2nd

private property

private variables

product types2nd.

    See also compound types.

Promise2nd3rd

Promise class

Promise.all() function2nd

Promise.race() function2nd

Promise.resolve() function

Promise.then() function

promises2nd3rd

  chaining

  chaining synchronous functions

  creating2nd

  handling errors

proofs

proofs-as-programs

property

pthread_create() function

public property

Pair type

parameterizing behavior

positive infinity

predicates2nd3rd

primitive obsession antipattern

primitive types2nd

private property

private variables

product types2nd.

    See also compound types.

Promise2nd3rd

Promise class

Promise.all() function2nd

Promise.race() function2nd

Promise.resolve() function

Promise.then() function

promises2nd3rd

  chaining

  chaining synchronous functions

  creating2nd

  handling errors

proofs

proofs-as-programs

property

pthread_create() function

public property

Q

quadratic time (O(n^2))

quadratic time (O(n^2))

R

R

random-access iterators

read-only variables

readonly properties

record types

reduce() function2nd3rd4th5th6th

  filter/reduce pipeline

  generic versions of

reduceRight() method

reference types

references

  associative arrays

  binary trees

  implementation trade-offs

  list efficiency

  overview

reject() function

rejected state, promise

resolve() function

resumable counters

reusable identity functions

reverse() function2nd3rd

run time

random-access iterators

read-only variables

readonly properties

record types

reduce() function2nd3rd4th5th6th

  filter/reduce pipeline

  generic versions of

reduceRight() method

reference types

references

  associative arrays

  binary trees

  implementation trade-offs

  list efficiency

  overview

reject() function

rejected state, promise

resolve() function

resumable counters

reusable identity functions

reverse() function2nd3rd

run time

小号

S

saturation

sealed keyword

second-order function

security

select() function2nd

serialization

settled state, promise

shape-preserving operations

short circuit evaluation

single-responsibility principle

skip() function

state machines2nd

  early programming with types

  implementing

  overview

state space

static typing

strategy pattern

  first-class functions

  function types

  implementing

streaming data

strict settings

string type2nd3rd4th

strings

  breaking text

  encoding libraries

  encodings

    UTF-16

    UTF-32

    UTF-8

strong typing

struct type2nd

structural subtyping

subtyping2nd3rd4th

  collections and

  distinguishing between similar types

    simulating nominal subtyping

    structural vs. nominal subtyping

  function argument types and

  function return types and

  sum types and

  types that can be assigned to anything

  types to which anything can be assigned

sum types5th.

    See also either-or types.

  as alternative to OOP

  heterogenous collections

  subtyping and

switch statements2nd



synchronous execution

  chaining synchronous functions

  of long-running operations

System.Linq namespace2nd

saturation

sealed keyword

second-order function

security

select() function2nd

serialization

settled state, promise

shape-preserving operations

short circuit evaluation

single-responsibility principle

skip() function

state machines2nd

  early programming with types

  implementing

  overview

state space

static typing

strategy pattern

  first-class functions

  function types

  implementing

streaming data

strict settings

string type2nd3rd4th

strings

  breaking text

  encoding libraries

  encodings

    UTF-16

    UTF-32

    UTF-8

strong typing

struct type2nd

structural subtyping

subtyping2nd3rd4th

  collections and

  distinguishing between similar types

    simulating nominal subtyping

    structural vs. nominal subtyping

  function argument types and

  function return types and

  sum types and

  types that can be assigned to anything

  types to which anything can be assigned

sum types5th.

    See also either-or types.

  as alternative to OOP

  heterogenous collections

  subtyping and

switch statements2nd



synchronous execution

  chaining synchronous functions

  of long-running operations

System.Linq namespace2nd

T

tagged union types

tagged union types (Variant types)

take() function2nd

Task

text-breaking function

then() function2nd3rd4th

third-order function

threads

throw statement

top types

transform() function2nd

traversing data structures

  streamlining iteration code

  using iterators

tuple types

two’s complement encoding

type casting2nd3rd

  common type casts

    downcasts

    narrowing casts

    upcasts

    widening casts

  overview

  tracking types outside type system

type checking

type constructors

type guards

type inference2nd

type parameter constraints

  generic algorithms with

  generic data structures with

type safety

  enforcing constraints

    with constructor

    with factory

  hiding and restoring type information

    heterogenous collections

    serialization

  preventing misinterpretation

    Mars Climate Orbiter

    primitive obsession antipattern

  type casting

    common type casts

    overview

    tracking types outside type system

type systems

  benefits of

    composability

    correctness

    encapsulation

    immutability

    readability

  data interpretation

  defined

  purpose of

  types of

    dynamic typing

    static typing

    strong typing

    type inference

    weak typing

  types, defined

types

  arrays and references

    associative arrays

    binary trees

    fixed-size arrays

    implementation trade-offs

    list efficiency

    references

  Boolean types

    Boolean expressions

    short circuit evaluation

  combining

    algebraic data types

    compound types

    either-or types

    visitor design pattern

  defined

  empty types

  function types

    counters

    decorator pattern

    higher-order functions

    lazy values

    long-running operations

    simplifying asynchronous code

    state machines without switch statements

    strategy pattern

  higher kinded types

    category theory and

    dependent types

    functional programming

    generic programming

    linear types

    map

    monads

  numerical types

    arbitrarily large numbers

    floating-point types

    integer types

  strings

    breaking text

    encoding libraries

    encodings

  unit types

TypeScript2nd3rd4th5th6th7th8th9th10th11th12th13th14th15th16th17th

  cheat sheet

  installing locally

  online playground

  source code

tagged union types

tagged union types (Variant types)

take() function2nd

Task

text-breaking function

then() function2nd3rd4th

third-order function

threads

throw statement

top types

transform() function2nd

traversing data structures

  streamlining iteration code

  using iterators

tuple types

two’s complement encoding

type casting2nd3rd

  common type casts

    downcasts

    narrowing casts

    upcasts

    widening casts

  overview

  tracking types outside type system

type checking

type constructors

type guards

type inference2nd

type parameter constraints

  generic algorithms with

  generic data structures with

type safety

  enforcing constraints

    with constructor

    with factory

  hiding and restoring type information

    heterogenous collections

    serialization

  preventing misinterpretation

    Mars Climate Orbiter

    primitive obsession antipattern

  type casting

    common type casts

    overview

    tracking types outside type system

type systems

  benefits of

    composability

    correctness

    encapsulation

    immutability

    readability

  data interpretation

  defined

  purpose of

  types of

    dynamic typing

    static typing

    strong typing

    type inference

    weak typing

  types, defined

types

  arrays and references

    associative arrays

    binary trees

    fixed-size arrays

    implementation trade-offs

    list efficiency

    references

  Boolean types

    Boolean expressions

    short circuit evaluation

  combining

    algebraic data types

    compound types

    either-or types

    visitor design pattern

  defined

  empty types

  function types

    counters

    decorator pattern

    higher-order functions

    lazy values

    long-running operations

    simplifying asynchronous code

    state machines without switch statements

    strategy pattern

  higher kinded types

    category theory and

    dependent types

    functional programming

    generic programming

    linear types

    map

    monads

  numerical types

    arbitrarily large numbers

    floating-point types

    integer types

  strings

    breaking text

    encoding libraries

    encodings

  unit types

TypeScript2nd3rd4th5th6th7th8th9th10th11th12th13th14th15th16th17th

  cheat sheet

  installing locally

  online playground

  source code

ü

U

undefined type2nd3rd4th5th6th7th8th9th

underflow

  detecting

  overview

underscore.js package

Unicode2nd

uninhabitable types

unique symbol trick2nd3rd

unit types

unit() function2nd

unknown type2nd3rd4th5th

upcasts

UTF-16 encoding

UTF-32 encoding

UTF-8 encoding

undefined type2nd3rd4th5th6th7th8th9th

underflow

  detecting

  overview

underscore.js package

Unicode2nd

uninhabitable types

unique symbol trick2nd3rd

unit types

unit() function2nd

unknown type2nd3rd4th5th

upcasts

UTF-16 encoding

UTF-32 encoding

UTF-8 encoding

V

V

value property2nd

value types

variable-length encodings

Variant types (tagged union types)2nd

visit() function2nd3rd4th

visitor design pattern

  alternative implementation of

  naïve implementation

  naïve implementation of

  variant visitor function

void property2nd

void type2nd3rd4th5th

value property2nd

value types

variable-length encodings

Variant types (tagged union types)2nd

visit() function2nd3rd4th

visitor design pattern

  alternative implementation of

  naïve implementation

  naïve implementation of

  variant visitor function

void property2nd

void type2nd3rd4th5th

W

W

weak typing

well-formed values

where() function2nd

widening casts2nd

wrap around

weak typing

well-formed values

where() function2nd

widening casts2nd

wrap around

Y

yield keyword

yield statement2nd3rd

yield keyword

yield statement2nd3rd

Z

Z

zip() function

zip() function

图列表

List of Figures

第 1 章类型简介

Chapter 1. Introduction to typing

图 1.1。比特序列可以用多种方式解释。

图 1.2。类型为带符号的 16 位整数的位序列。类型信息(16 位带符号整数)告诉编译器和/或运行时,位序列表示 -32768 和 32767 之间的整数值,确保正确解释为 -15709。

图 1.3。源代码由编译器或解释器转换为可由运行时执行的代码。运行时是物理计算机或虚拟机,比如Java的JVM,或者浏览器的JavaScript引擎。

图 1.4。正确声明类型,我们可以禁止无效值。第一种类型过于宽松,允许我们不想要的值。如果代码试图将不需要的值分配给变量,则第二种限制性更强的类型将无法编译。

Figure 1.1. A sequence of bits can be interpreted in multiple ways.

Figure 1.2. The sequence of bits typed as a signed 16-bit integer. The type information (16-bit signed integer) tells the compiler and/or run time that the sequence of bits represents an integer value between -32768 and 32767, ensuring the correct interpretation as -15709.

Figure 1.3. Source code is transformed by a compiler or interpreter into code that can be executed by a run time. The run time is a physical computer or a virtual machine, such as Java’s JVM, or a browser’s JavaScript engine.

Figure 1.4. Declaring a type correctly, we can disallow invalid values. The first type is too loose and allows for values we don’t want. The second, more restrictive type won’t compile if the code tries to assign an unwanted value to a variable.

第 2 章基本类型

Chapter 2. Basic types

图 2.1。AND、OR 和 NOT 真值表

图 2.2。4 位无符号整数编码。当所有 4 位都为 0 时,最小可能值是 0。当所有位都为 1 时,最大可能值是 15 (1 * 23 + 1 * 22 + 1 * 21 + 1 * 20)。

图 2.3。4 位有符号整数编码。–8 编码为 24 – 8(二进制 1000),–3 编码为 24 – 3(二进制 1101)。对于负数,第一位始终为 1,对于正数,第一位始终为 0。

图 2.4。处理算术溢出的不同方法。里程表从 999999 回绕到 0;一个旋钮简单地停在可能的最大值;袖珍计算器打印错误并停止。

图 2.5。0.10 的浮点表示。首先,我们看到三个组成部分在内存中的二进制表示:符号位、指数和尾数。下面,我们有将二进制表示形式转换为数字的公式。最后,我们看到应用公式的结果:0.10 近似于 0.100000000000000005551115123126。

图 2.6。女警官表情符号的字符编码(警官表情符号字符 + 零宽度连接符 + 女性符号表情符号)和生成的字素(女警官)。

图 2.7。女警官表情符号被视为内存中位的 UTF-16 字符串编码、UTF-16 字节序列、Unicode 代码点序列和字素。

图 2.8。五个 32 位整数存储在一个固定大小的数组和一个链表中。在固定大小的数组中查找元素非常快,因为我们可以计算出它的确切位置。另一方面,链表要求我们跟随下一个元素,直到找到我们要查找的元素。元素可以在内存中的任何地方。

图 2.9。一个基于数组的列表,包含 9 个元素,容量为 16。在必须将数据移动到更大的数组之前,可以附加七个元素。

图 2.10。二叉树表示为固定大小的数组。缺少的节点(2 的右子节点)是数组中未使用的元素。节点之间的父子关系是隐式的,因为可以根据父节点的索引计算子节点的索引,反之亦然。

图 2.11。只有三个节点的稀疏二叉树仍然需要一个包含七个元素的数组才能正确表示。如果节点 9 有一个孩子,数组大小将变为 15。

图 2.12。使用引用表示的稀疏树。右图将节点数据结构表示为值、左引用、右引用。

图 2.13。作为列表数组实现的关联数组。此实例包含键值映射 0 → 10、→ 9、5 → 10 和 42 → 0。

Figure 2.1. AND, OR, and NOT truth tables

Figure 2.2. 4-bit unsigned integer encoding. Smallest possible value, when all 4 bits are 0, is 0. Largest possible value, when all bits are 1, is 15 (1 * 23 + 1 * 22 + 1 * 21 + 1 * 20).

Figure 2.3. 4-bit signed integer encoding. –8 is encoded as 24 – 8 (1000 binary), and –3 is encoded as 24 – 3 (1101 binary). The first bit is always 1 for negative numbers and 0 for positive numbers.

Figure 2.4. Different ways to handle arithmetic overflow. An odometer wraps around from 999999 back to 0; a dial knob simply stops at the maximum possible value; a pocket calculator prints Error and stops.

Figure 2.5. Floating-point representation of 0.10. First, we see the in-memory binary representation of the three components: sign bit, exponent, and mantissa. Below, we have the formula to convert the binary representation to a number. Finally, we see the result of applying the formula: 0.10 is approximated to 0.100000000000000005551115123126.

Figure 2.6. Character encoding of the woman police-officer emoji (police-officer emoji character + zero-width joiner + female sign emoji) and resulting grapheme (woman police officer).

Figure 2.7. Woman police-officer emoji viewed as UTF-16 string encoding of bits in memory, UTF-16 byte sequence, sequence of Unicode code points, and grapheme.

Figure 2.8. Five 32-bit integers stored in a fixed-size array and in a linked list. Finding an element is extremely fast in the fixed-size array, as we can compute its exact location. On the other hand, a linked list requires us to follow the next elements until we find the element we are looking for. Elements can be anywhere in memory.

Figure 2.9. An array-based list with 9 elements and capacity for 16. Seven more elements can be appended before the data has to be moved to a larger array.

Figure 2.10. Binary tree represented as a fixed-size array. The missing node (right child of 2) is an unused element in the array. The parent–child relation between the nodes is implicit, as the index of a child can be computed from the index of the parent, and vice versa.

Figure 2.11. A sparse binary tree with only three nodes still requires an array with seven elements to be represented correctly. If node 9 had a child, the array size would become 15.

Figure 2.12. Sparse tree represented by using references. The diagram on the right represents the node data structure as value, left reference, right reference.

Figure 2.13. Associative array implemented as an array of lists. This instance contains the key-value mappings 0 → 10, → 9, 5 → 10, and 42 → 0.

第三章作文

Chapter 3. Composition

图 3.1。组合两种类型,以便生成的类型包含它们各自的值。每个表情符号代表其中一种类型的值。括号将组合类型的值表示为原始类型的成对值。

图 3.2。对 (1, 5) 的两种解释方式:作为 X 坐标 1 和 Y 坐标 5 的点 A,或作为 X 坐标 5 和 Y 坐标 1 的点 B。

图 3.3。组合两种类型,以便生成的类型允许来自两种类型之一的值。

图 3.4。Result 类型的所有可能值作为 InputError 和 DayOfWeek 的组合。那是 21 个值(3 InputError x 7 DayOfWeek)。

图 3.5。Result 类型的所有可能值作为 InputError 或 DayOfWeek 的组合。那是 9 个值(2 InputError + 7 DayOfWeek)。我们不再需要 OK InputError,因为我们有 DayOfWeek 值这一事实表明没有错误。

图 3.6。访客模式。IDocumentItem 接口确保每个文档项都有一个接受 IVisitor 的 accept() 方法。IVisitor 确保每个访问者都可以处理所有可能的文档项类型。每个文档项都实现 accept() 以将其自身发送给访问者。使用这种模式,我们可以将职责(例如屏幕呈现和可访问性)分离到各个组件(访问者),并将它们从文档项中抽象出来。

图 3.7。简化的访问者模式:现在文档项和访问者不需要实现任何接口。将此图与图 3.6 进行对比。将文档项与正确的访问者方法相匹配的职责封装在 visit() 方法中。从图中我们可以看出,类型是不相关的,这是一件好事:它使我们的程序更加灵活。

Figure 3.1. Combining two types so that the resulting type contains a value from each of them. Each emoji represents a value from one of the types. The parentheses represent the values of the combined type as pairs of values from the original types.

Figure 3.2. Two ways to interpret the pair (1, 5): as point A with X coordinate 1 and Y coordinate 5, or as point B with X coordinate 5 and Y coordinate 1.

Figure 3.3. Combining two types so that the resulting type allows values from either of the two types.

Figure 3.4. All possible values of the Result type as combinations of InputError and DayOfWeek. That’s 21 values (3 InputError x 7 DayOfWeek).

Figure 3.5. All possible values of Result type as a combination of InputError or DayOfWeek. That’s 9 values (2 InputError + 7 DayOfWeek). We no longer need an OK InputError, as the absence of an error is indicated by the fact that we have a DayOfWeek value.

Figure 3.6. A visitor pattern. The IDocumentItem interface ensures that every document item has an accept() method that takes an IVisitor. IVisitor ensures that every visitor can handle all possible document item types. Each document item implements accept() to send itself to the visitor. With this pattern, we can separate responsibilities, such as screen rendering and accessibility, to individual components (visitors) and abstract them away from the document items.

Figure 3.7. A simplified visitor pattern: now the document items and visitors don’t need to implement any interfaces. Contrast this figure with figure 3.6. Responsibility for matching a document item with the right visitor method is encapsulated in the visit() method. As we can see from the figure, the types are not related, which is a good thing: it makes our program more flexible.

第 4 章类型安全

Chapter 4. Type safety

图 4.1。数值 1000 可以代表 1,000 美元或 1,000 英里。两个不同的开发人员可以将其解释为两种截然不同的措施。

图 4.2。具有明确的货币类型可以清楚地表明该值不代表 1,000 英里,而是代表美元金额。

图 4.3。通过转换,我们可以将 16 位有符号整数类型的值转换为 UTF-8 编码字符。

图 4.4。如果我们有一个三角形或一个正方形,我们不能确定我们拥有的实际形状是否会通过三角形槽。如果它是三角形会,但如果它是正方形则不会。

图 4.5。扩大和缩小铸件的示例。加宽转换是安全的:灰色方块代表我们得到的额外位,因此不会丢失任何信息。另一方面,缩小转换是危险的:黑色方块代表不再适合新类型的位。

图 4.6。如果我们有一个只装猫的袋子,我们可以打赌,无论我们从袋子里拿出什么东西,都会是一只猫。如果袋子里也能装杂货,我们就不能再保证能拿出什么东西了。

图 4.7。具有两扇门和前轮驱动的紧凑型汽车序列化为 JSON,然后反序列化回汽车

Figure 4.1. The numeric value 1000 could represent 1,000 dollars or 1,000 miles. Two different developers could interpret it as two very different measures.

Figure 4.2. Having an explicit Currency type makes it clear that the value does not represent 1,000 miles, but rather a dollar amount.

Figure 4.3. With casting, we can turn a value of type 16-bit signed integer into a UTF-8 encoded character.

Figure 4.4. If we have a triangle or a square, we can’t say for sure whether the actual shape we have will pass through a triangular slot. It will if it’s a triangle, but it won’t if it’s a square.

Figure 4.5. Example of widening and narrowing casts. The widening cast is safe: the gray squares represent the extra bits we get, so no information can be lost. On the other hand, the narrowing cast is dangerous: the black squares represent bits that no longer fit in the new type.

Figure 4.6. If we have a bag that contains only cats, we can bet that whichever item we pull out of it will be a cat. If the bag can also contain groceries, we are no longer able to guarantee what we will pull out.

Figure 4.7. A compact car with two doors and front-wheel drive serialized as JSON and then deserialized back into a car

第 5 章函数类型

Chapter 5. Function types

图 5.1。策略模式由 IStrategy 接口、ConcreteStrategy1 和 ConcreteStrategy2 实现以及通过 IStrategy 接口使用算法的 Context 组成。

图 5.2。由使用函数的 Context 组成的策略模式:concreteStrategy1() 或 concreteStrategy2()

图 5.3。两个包含代码示例的 TypeScript (.ts) 文件应内联在 Markdown 文档中的 ```ts 和 ``` 标记之间。<!-- ... --> 注释注释了我的脚本的代码示例。

图 5.4。文本处理状态机,具有三种状态(文本处理、标记处理、代码处理)和基于输入的状态之间的转换。文本处理是初始状态或开始状态。

图 5.5。对数字求平方和获取字符串长度是截然不同的场景,但转换的整体结构是相同的:采用输入数组,应用函数,然后生成输出数组。

图 5.6。甚至长度为 5 的数字和字符串共享一个结构。我们遍历输入,应用过滤器,并输出过滤器返回 true 的项目。

图 5.7。组合数字数组中的数字和字符串数组中的字符串的通用结构。在第一种情况下,初始值为 1,我们应用的组合是与每个项目相乘。在第二种情况下,初始值为“”,我们应用的组合是与每个项目的连接。

图 5.8。将字符串数组与“两个字符串的第一个字母”操作相结合,在从左到右和从右到左应用时会给出不同的结果。在第一种情况下,我们从一个空字符串和“apple”开始,然后是“a”和“orange”,然后是“ao”和“peach”,最后得到“ap”。在第二种情况下,我们从“peach”和一个空字符串开始,然后是“orange”和“p”,得到“op”;然后是“apple”和“op”,给我们“ao”。

Figure 5.1. Strategy pattern made up of an IStrategy interface, ConcreteStrategy1 and ConcreteStrategy2 implementations, and a Context that uses the algorithms through the IStrategy interface.

Figure 5.2. Strategy pattern made up of a Context that uses a function: either concreteStrategy1() or concreteStrategy2()

Figure 5.3. Two TypeScript (.ts) files containing code samples that should be inlined in the Markdown document between ```ts and ``` markers. The <!-- ... --> comments annotate the code samples for my script.

Figure 5.4. Text processing state machine with the three states (text processing, marker processing, code processing) and transitions between the states based on input. Text processing is the initial state or start state.

Figure 5.5. Squaring numbers and getting string lengths are very different scenarios, but the overall structure of the transformation is the same: take an input array, apply a function, and produce an output array.

Figure 5.6. Even numbers and strings with length 5 share a structure. We traverse the input, apply the filter, and output the items for which the filter returns true.

Figure 5.7. Common structure of combining the numbers in a number array and strings in a string array. In the first case, the initial value is 1, and the combination we apply is multiplication with each item. In the second case, the initial value is "", and the combination we apply is concatenation with each item.

Figure 5.8. Combining an array of strings with the operation “first letter of both strings” gives us different results when applied from left to right and when applied from right to left. In the first case, we start with an empty string and "apple", then "a" and "orange" , then "ao" and "peach", giving us "ap". In the second case, we start with "peach" and an empty string, followed by "orange" and "p", giving us "op"; and then "apple" and "op", giving us "ao".

第 6 章函数类型的高级应用

Chapter 6. Advanced applications of function types

图 6.1。装饰器模式:一个 IComponent 接口,一个通过 ConcreteComponent 的具体实现,以及一个通过附加行为增强 IComponent 的装饰器

图 6.2。小部件工厂的装饰器模式。IWidgetFactory 是接口,WidgetFactory 是具体实现,SingletonDecorator 是给一个IWidgetFactory 添加单例行为。

图 6.3。函数式装饰器:我们现在只有一个 makeWidget() 函数和一个 singletonDecorator() 函数。

图 6.4。一个返回闭包的简单函数:一个引用函数局部变量的 lambda。即使在 getClosure() 返回之后,这个变量仍然被闭包引用,所以它比它出现的函数还长。

图 6.5。重要的是要理解每个闭包(在我们的例子中是 counter1 和 counter2)以不同的 n 结束。每当我们调用 makeCounter() 时,一个新的 n 被初始化为 1 并被返回的闭包捕获。因为这些值是分开的,所以它们不会相互干扰。

图 6.6。createThread() 创建一个新线程。原线程继续执行operation1(),然后operation2(),新线程并行执行longRunningOperation()。

图 6.7。countDown() 计数一步;然后它产生并允许其他代码运行。它还使用递减的计数器值排队另一个对 countDown() 的调用。如果计数器达到 0,则 countDown() 不会将对自身的另一个调用排入队列。

图 6.8。每个计数器运行,然后将另一个操作排入队列。执行按照操作入队的顺序进行。一切都在一个线程上运行。

图 6.9。getUserName() 使代码排队以获取用户名并返回 Promise<string>。getUserName() 的调用者可以调用 then() 来承诺连接 getUserEmail() 延续——当我们有用户名时要运行的代码。稍后,获取用户名的代码运行并使用用户名调用 resolve()。此时,使用现在可用的用户名调用延续 getUserEmail()。

图 6.10。一个 promise 从 pending 状态开始。(getUserName() 安排了代码来提示用户,但 question() 尚未返回。)resolve() 将其转换为已解决状态并调用继续(如果提供)(在用户提供他们的姓名之后)。一个值可用,因此可以调用延续(getUserEmail(),在我们的例子中)。reject() 将承诺转换为拒绝状态并调用错误处理延续(如果提供的话)。值不可用;相反,错误的原因是可用的。

图 6.11。组合承诺的不同方式。然后:Promise 1 结算并分发 Value 1 给 Promise 2;Promise 2 结算并将值 2 分发给 Promise 3。所有:Promise 1、2 和 3 结算。当所有这些都已解决时,Promise.all 将获得它们的所有值并可以继续,解决自己的价值。种族:其中一个承诺首先解决(在本例中为承诺 2)。Promise.race 获得 Value 2 并可以继续,确定它自己的值。

Figure 6.1. Decorator pattern: an IComponent interface, a concrete implementation via ConcreteComponent, and a Decorator that enhances an IComponent with additional behavior

Figure 6.2. Decorator pattern for the widget factory. IWidgetFactory is the interface, WidgetFactory is a concrete implementation, and SingletonDecorator adds singleton behavior to an IWidgetFactory.

Figure 6.3. Functional decorator: we now have only a makeWidget() function and a singletonDecorator() function.

Figure 6.4. A simple function that returns a closure: a lambda that references a variable local to the function. Even after getClosure() returns, the variable is still referenced by the closure, so it outlives the function in which it appeared.

Figure 6.5. It’s important to understand that each closure (in our case, counter1 and counter2) ends up with a different n. Whenever we call makeCounter(), a new n is initialized to 1 and captured by the returned closure. Because the values are separate, they don’t interfere with each other.

Figure 6.6. createThread() creates a new thread. The original thread continues to execute operation1() and then operation2(), and the new thread executes longRunningOperation() in parallel.

Figure 6.7. countDown() counts one step; then it yields and allows other code to run. It also enqueus another call to countDown() with the decremented counter value. If the counter reaches 0, countDown() doesn’t enqueue another call to itself.

Figure 6.8. Each counter runs and then enqueues another operation. Execution proceeds in the order in which operations are enqueued. Everything runs on a single thread.

Figure 6.9. getUserName() enqueues the code to get the username and returns a Promise<string>. The caller of getUserName() can call then() on the promise to hook up the getUserEmail() continuation—code to be run when we have a username. At some later time, the code to get the user name runs and calls resolve() with the username. At this point, the continuation getUserEmail() gets called with the now-available user name.

Figure 6.10. A promise starts in the pending state. (getUserName() scheduled the code to prompt the user, but question() hasn’t returned yet.) resolve() transitions it to the settled state and invokes a continuation if one is provided (after the user provided their name). A value is available so the continuation can be called (getUserEmail(), in our case). reject() transitions the promise to the rejected state and invokes an error-handling continuation, if one is provided. A value is not available; a reason for the error is available instead.

Figure 6.11. Different ways to combine a promise. Then: Promise 1 settles and hands out Value 1 to Promise 2; Promise 2 settles and hands out Value 2 to Promise 3. All: Promise 1, 2, and 3 settle. When all of them are settled, Promise.all gets all their values and can proceed, settling its own value. Race: One of the promises settles first (in this case, Promise 2). Promise.race gets Value 2 and can proceed, settling its own value.

第 7 章子类型化

Chapter 7. Subtyping

图 7.1。顶级类型是任何其他类型的超类型。我们可以定义任意数量的类型,但它们中的任何一个都将是顶级类型的子类型。我们可以在需要顶级类型的地方使用任何类型的值。

图 7.2。底层类型是任何其他类型的子类型。我们可以定义任意数量的类型,但其中任何一个都将是底层类型的超类型。我们可以在任何需要任何类型的值的地方传递一个底层类型的值(尽管我们永远不能产生这样的值)。

图 7.3。三角形 | 正方形是三角形的子类型 | 方形 | Circle 因为只要需要三角形、正方形或圆形,我们就可以使用三角形或正方形。

图 7.4。如果 Triangle 是 Shape 的子类型,则三角形数组是形状数组的子类型。如果我们可以将 Triangle 用作 Shape,则可以将 Triangle 对象数组用作 Shape 对象数组。

图 7.5。如果 Triangle 是 Shape 的子类型,我们可以使用返回 Triangle 的函数而不是返回 Shape 的函数,因为我们总是可以将 Triangle 分配给需要 Shape 的调用者。

图 7.6。如果 Triangle 是 Shape 的子类型,我们可以使用一个期望 Shape 作为参数的函数,而不是一个期望 Triangle 作为参数的函数,因为我们总是可以将 Triangle 传递给接受 Shape 的函数。

图 7.7。如果 Triangle 是 Shape 的子类型,在 TypeScript 中,可以使用期望 Triangle 的函数而不是期望 Shape 的函数,并且可以使用期望 Shape 的函数而不是期望 Triangle 的函数。

Figure 7.1. A top type is the supertype of any other type. We can define any number of types, but any of them would be a subtype of the top type. We can use a value of any type wherever the top type is expected.

Figure 7.2. A bottom type is the subtype of any other type. We can define any number of types, but any of these would be a supertype of the bottom type. We can pass a value of the bottom type wherever a value of any type is required (although we can never produce such a value).

Figure 7.3. Triangle | Square is a subtype of Triangle | Square | Circle because whenever a Triangle, Square, or Circle is expected, we can use a Triangle or a Square.

Figure 7.4. If Triangle is a subtype of Shape, an array of triangles is a subtype of an array of shapes. If we can use a Triangle as a Shape, we can use an array of Triangle objects as an array of Shape objects.

Figure 7.5. If Triangle is a subtype of Shape, we can use a function that returns a Triangle instead of a function that returns a Shape because we can always assign a Triangle to a caller that expects a Shape.

Figure 7.6. If Triangle is a subtype of Shape, we can use a function that expects a Shape as argument instead of a function that expects a Triangle as argument because we can always pass a Triangle to a function that takes a Shape.

Figure 7.7. If Triangle is a subtype of Shape, in TypeScript, a function that expects a Triangle can be used instead of a function that expects a Shape, and a function that expects a Shape can be used instead of a function that expects a Triangle.

第 8 章面向对象编程的要素

Chapter 8. Elements of object-oriented programming

图 8.1。所有的动物都吃。我们可以和宠物一起玩(但它们仍然需要吃饭)。猫也会喵喵叫(但它们仍然会玩耍和吃东西)。

图 8.2。以 BinaryExpression 作为父项,SumExpression 和 MulExpression 作为子项的表达式层次结构

图 8.3。所有形状都有一个 ID。圆是一种形状,所以它继承了id。圆有一个定义其中心的点。

图 8.4。我们有一个不兼容的 IExpected 接口和 Adaptee 实际实现。适配器通过提供 IExpected 的实现并处理 IExpected 声明的内容和 Adaptee 提供的内容之间的转换来使它们兼容。

图 8.5。使用 WildAnimal 和 Wolf 扩展动物等级。野生动物可以 roam(),狼可以使用其 track()、stalk() 和 pounce() 方法狩猎。

图 8.6。Hunter 类型是 Wolf 和 Tiger 的父类,提供狩猎行为。

图 8.7。Cat、Wolf 和 Tiger 封装了 HunterBehavior 的实例并实现了 IHunter 接口。他们将所有调用转发给包装对象。HunterBehavior 提供了 IHunter 的实现,所有实现 IHunter 的动物都可以将其用作组件。HunterBehavior 不再是 Animal 层次结构的一部分。

图 8.8。Cat、Wolf 和 Tiger 混合在 HunterBehavior 中,它删除了一堆样板:这些类不再需要包装 HunterBehavior 对象并转发调用。他们可以简单地包括行为。

图 8.9。面向对象的策略模式。在 ConcreteStrategy1 和 ConcreteStrategy2 中实现了不同版本的算法。

图 8.10。功能策略模式。算法的不同版本被实现为函数。

Figure 8.1. All animals eat. We can play with pets (but they still need to eat). Cats also meow (but they still play and eat).

Figure 8.2. Expression hierarchy with BinaryExpression as parent and SumExpression and MulExpression as children

Figure 8.3. All shapes have an id. A circle is a shape, so it inherits the id. A circle has a point that defines its center.

Figure 8.4. We have an IExpected interface and an Adaptee actual implementation that are incompatible. The Adapter makes them compatible by providing an implementation of IExpected and handling translation between what IExpected declares and what the Adaptee provides.

Figure 8.5. Extended animal hierarchy with WildAnimal and Wolf. Wild animals can roam(), and a wolf can hunt with its track(), stalk(), and pounce() methods.

Figure 8.6. The Hunter type is the parent of Wolf and Tiger, and provides hunting behavior.

Figure 8.7. Cat, Wolf, and Tiger wrap an instance of HunterBehavior and implement the IHunter interface. They forward all calls to the wrapped object. HunterBehavior provides an implementation of IHunter that all animals implementing IHunter can use as a component. HunterBehavior is no longer part of the Animal hierarchy.

Figure 8.8. Cat, Wolf, and Tiger mix in HunterBehavior, which removes a bunch of boilerplate: the classes no longer need to wrap a HunterBehavior object and forward calls. They can simply include the behavior.

Figure 8.9. Object-oriented strategy pattern. Different versions of an algorithm are implemented in ConcreteStrategy1 and ConcreteStrategy2.

Figure 8.10. Functional strategy pattern. Different versions of an algorithm are implemented as functions.

第 9 章通用数据结构

Chapter 9. Generic data structures

图 9.1。具有类型参数 T 和两个实例的通用标识:identity<number>() 具有具体类型 (value: number) => number 和 identity<Widget[]>() 具有具体类型 (value: Widget[]) = > 小部件[]

图 9.2。中序遍历。递归地向左走,直到我们到达最左边的节点,转到它的父节点,然后转到右边的节点。接下来,我们回到父节点的父节点,然后转到它的右节点。订单总是被留下;然后,当所有子树都被访问时,父节点;然后是对的。

图 9.3。二叉树示例

图 9.4。链表示例

图 9.5。inOrder() 按顺序遍历二叉树并将所有值添加到队列中。next() 出列值并在遍历期间返回它们。

图 9.6。BinaryTreeIterator 实现二叉树遍历。LinkedListIterator 实现链表遍历。两者都实现了 Iterator 契约。print() 和 contains() 以 Iterator 作为参数,因此我们可以混合和匹配具有不同数据结构的函数。

图 9.7。inOrderIterator() 是一个生成器,因此它返回一个 IterableIterator<T>。与 inOrder() 一样,此函数递归地遍历树,但不是对项目进行排队,而是产生它们。在返回的迭代器上调用 next() 恢复生成器并产生下一个值。

图 9.8。流水线和调用顺序。take() 从 square() 的迭代器中请求一个值。square() 从 generateRandomNumber() 的迭代器请求一个值。generateRandomNumbers() 产生一个值给 square()。square() 产生一个值给 take()。

Figure 9.1. Generic identity with a type parameter T and two instances: identity<number>() with the concrete type (value: number) => number and identity<Widget[]>() with the concrete type (value: Widget[]) => Widget[]

Figure 9.2. In-order traversal. Recursively go left until we reach the leftmost node, go to its parent, and then go to the right node. Next, we go back to the parent of the parent and then go to its right node. The order is always left; then, when that subtree is all visited, parent; and then right.

Figure 9.3. Binary tree example

Figure 9.4. Linked list example

Figure 9.5. inOrder() traverses the binary tree in order and adds all values to a queue. next() dequeues values and returns them during traversal.

Figure 9.6. BinaryTreeIterator implements binary tree traversal. LinkedListIterator implements linked list traversal. Both implement the Iterator contract. print() and contains() take an Iterator as argument, so we can mix and match the functions with different data structures.

Figure 9.7. inOrderIterator() is a generator, so it returns an IterableIterator<T>. Like inOrder(), this function recursively traverses the tree, but instead of queuing items, it yields them. Calling next() on the returned iterator resumes the generator and yields the next value.

Figure 9.8. Pipeline and call sequence. take() requests a value from square()’s iterator. square() requests a value from generateRandomNumber()’s iterator. generateRandomNumbers() yields a value to square(). square() yields a value to take().

第 10 章通用算法和迭代器

Chapter 10. Generic algorithms and iterators

图 10.1。用堆栈反转序列:原始序列中的元素被压入堆栈,然后弹出以产生反转序列。

图 10.2。通过交换元素来原地反转数组

图 10.3。begin 和 end 迭代器定义一个范围:begin 指向第一个元素,end 指向最后一个元素。

图 10.4。输入迭代器可以检索当前元素的值并前进到下一个元素。

图 10.5。前向迭代器可以读取和写入当前元素的值,前进到下一个元素,并创建一个支持多次遍历的自身克隆。在此图中,我们看到 clone() 如何创建迭代器的副本。当我们推进原件时,克隆件不会移动。

图 10.6。双向迭代器可以读取和写入当前元素的值、克隆自身以及向前和向后移动。

图 10.7。随机访问迭代器可以读取和写入当前元素的值、克隆自身以及向后或向前移动任意数量的步骤。

Figure 10.1. Reversing a sequence with a stack: elements from the original sequence get pushed on the stack and then popped to produce the reversed sequence.

Figure 10.2. Reversing an array in place by swapping its elements

Figure 10.3. begin and end iterators define a range: begin points to the first element, and end points past the last element.

Figure 10.4. An input iterator can retrieve the value of the current element and advance to the next element.

Figure 10.5. A forward iterator can read and write the value of the current element, advance to the next element, and create a clone of itself that enables multiple traversals. In this figure, we see how clone() creates a copy of the iterator. As we advance the original, the clone doesn’t move.

Figure 10.6. A bidirectional iterator can read and write the value of the current element, clone itself, and step both forward and backward.

Figure 10.7. A random-access iterator can read and write the value of the current element, clone itself, and move backward or forward any number of steps.

第 11 章高等类型及更高类型

Chapter 11. Higher kinded types and beyond

图 11.1。map() 接受一个序列的迭代器,在本例中是一个圆列表,以及一个转换圆的函数。map() 将函数应用于序列中的每个元素,并生成一个包含转换元素的新序列。

图 11.2。将函数映射到可选值。如果可选为空,map() 返回一个空的可选;否则,它将函数应用于值并返回包含结果的可选。

图 11.3。将函数映射到 Box 中的值。map() 从 Box 中解压值,应用函数,然后将值放回 Box 中。

图 11.4。我们有一个泛型类型 H,它包含 0、1 或更多类型 T 的值和一个从 T 到 U 的函数。在这种情况下,T 是一个空圆,而 U 是一个完整的圆。map() 仿函数从 H<T> 实例中解压缩 T 或 Ts,应用该函数,然后将结果放回 H<U> 中。

图 11.5。将一个函数映射到另一个函数上构成了这两个函数。结果是一个函数,它采用与原始函数相同的参数并返回第二个函数返回类型的值。这两个功能需要兼容;第二个函数必须期望一个与原始函数返回的参数类型相同的参数。

图 11.6。在这种情况下我们不能使用仿函数,因为仿函数被定义为将函数从白色圆圈映射到黑色圆圈。不幸的是,我们的函数返回的类型已经包含在 Either 中(Either<black square, black circle>)。我们需要一个可以处理此类函数的 map() 替代方法。

图 11.7。对比 map() 和 bind()。map() 在 Box<T> 上应用函数 T => U 并返回一个 Box<U>。bind() 在 Box<T> 上应用函数 T => Box><U> 并返回 Box<U>。

图 11.8。List monad:bind() 接受一个 Ts 序列(本例中为白色圆圈)和一个函数 T => Us 序列(本例中为黑色圆圈)。结果是我们的扁平列表(黑色圆圈)。

Figure 11.1. map() takes an iterator over a sequence, in this case a list of circles, and a function that transforms a circle. map() applies the function to each element in the sequence and produces a new sequence with the transformed elements.

Figure 11.2. Mapping a function over an optional value. If the optional is empty, map() returns an empty optional; otherwise, it applies the function to the value and returns an optional containing the result.

Figure 11.3. Mapping a function over a value in a Box. map() unpacks the value from the Box, applies the function, and then places the value back into a Box.

Figure 11.4. We have a generic type H that contains 0, 1, or more values of some type T and a function from T to U. In this case, T is an empty circle, and U is a full circle. The map() functor unpacks the T or Ts from the H<T> instance, applies the function, and then places the result back into an H<U>.

Figure 11.5. Mapping a function over another function composes the two functions. The result is a function that takes the same arguments as the original function and returns a value of the second function’s return type. The two functions need to be compatible; the second function must expect an argument of the same type as the one returned by the original function.

Figure 11.6. We can’t use a functor in this case because the functor is defined to map a function from a white circle to a black circle. Unfortunately, our function returns a type already wrapped in an Either (an Either<black square, black circle>). We need an alternative to map() that can deal with this type of function.

Figure 11.7. Contrasting map() and bind(). map() applies a function T => U over a Box<T> and returns a Box<U>. bind() applies a function T => Box><U> over a Box<T> and returns a Box<U>.

Figure 11.8. List monad: bind() takes a sequence of Ts (white circles, in this case) and a function T => sequence of Us (black circles, in this case). The result is a flattened list of Us (black circles).

房源清单

List of Listings

第 1 章类型简介

Chapter 1. Introduction to typing

清单 1.1。试图将数据解释为代码

清单 1.2。类型信息不足

清单 1.3。细化类型信息

清单 1.4。坏突变

清单 1.5。不变性

清单 1.6。封装不够

清单 1.7。封装

清单 1.8。不可组合的系统

清单 1.9。不可组合的系统更新

清单 1.10。组合系统

清单 1.11。无类型查找()

清单 1.12。输入查找()

清单 1.13。动态类型

清单 1.14。静态类型

清单 1.15。弱类型

清单 1.16。强类型

清单 1.17。类型推断

Listing 1.1. Trying to interpret data as code

Listing 1.2. Insufficient type information

Listing 1.3. Refined type information

Listing 1.4. Bad mutation

Listing 1.5. Immutability

Listing 1.6. Not enough encapsulation

Listing 1.7. Encapsulation

Listing 1.8. Noncomposable system

Listing 1.9. Noncomposable system update

Listing 1.10. Composable system

Listing 1.11. Untyped find()

Listing 1.12. Typed find()

Listing 1.13. Dynamic typing

Listing 1.14. Static typing

Listing 1.15. Weak typing

Listing 1.16. Strong typing

Listing 1.17. Type inference

第 2 章基本类型

Chapter 2. Basic types

清单 2.1。如果找不到配置文件,则引发并记录错误

清单 2.2。作为不可实例化类实现的空类型

清单 2.3。一个“你好世界!” 功能

清单 2.4。单元类型实现为无状态的单例

清单 2.5。看门人

清单 2.6。替代网守实施

清单 2.7。函数加总项目

清单 2.8。检查加法溢出

清单 2.9。币种及币种添加功能

清单 2.10。epsilon 内的浮点相等性

清单 2.11。简单的文本拆分功能

清单 2.12。使用 grapheme-splitter 库的文本断开功能

清单 2.13。链表实现

清单 2.14。基于数组的列表实现

清单 2.15。具有额外容量的基于数组的列表实现

清单 2.16。基于数组的二叉树实现

清单 2.17。紧凑的二叉树实现

Listing 2.1. Raising and logging an error if a config file is not found

Listing 2.2. Empty type implemented as an uninstantiable class

Listing 2.3. A “Hello world!” function

Listing 2.4. Unit type implemented as a singleton without state

Listing 2.5. Gatekeeper

Listing 2.6. Alternative gatekeeper implementation

Listing 2.7. Function adding up item total

Listing 2.8. Checking for addition overflow

Listing 2.9. Currency class and currency addition function

Listing 2.10. Floating-point equality within epsilon

Listing 2.11. Simple text-breaking function

Listing 2.12. Text-breaking function using grapheme-splitter library

Listing 2.13. Linked-list implementation

Listing 2.14. Array-based list implementation

Listing 2.15. Array-based list implementation with additional capacity

Listing 2.16. Array-based binary tree implementation

Listing 2.17. Compact binary tree implementation

第三章作文

Chapter 3. Composition

清单 3.1。两点之间的距离

清单 3.2。定义为元组的两点之间的距离

清单 3.3。对型

清单 3.4。定义为记录的两点之间的距离

清单 3.5。货币格式错误

清单 3.6。货币保持不变量

清单 3.7。不变的货币

清单 3.8。将星期几编码为数字

清单 3.9。用常量编码星期几

清单 3.10。将星期几编码为枚举

清单 3.11。将输入解析为 DayOfWeek 或未定义

清单 3.12。可选类型

清单 3.13。从函数返回结果和错误

清单 3.14。任一种类型

清单 3.15。从函数返回结果或错误

清单 3.16。标记的形状并集

清单 3.17。变异类型

清单 3.18。形状联合作为变体

清单 3.19。天真的实施

清单 3.20。用访客模式处理

清单 3.21。变异访客

清单 3.22。变体访问者的替代处理

Listing 3.1. Distance between two points

Listing 3.2. Distance between two points defined as tuples

Listing 3.3. Pair type

Listing 3.4. Distance between two points defined as records

Listing 3.5. Ill-formed currency

Listing 3.6. Currency maintaining invariants

Listing 3.7. Immutable Currency

Listing 3.8. Encoding day of week as a number

Listing 3.9. Encoding day of week with constants

Listing 3.10. Encoding day of week as an enum

Listing 3.11. Parsing input into a DayOfWeek or undefined

Listing 3.12. Optional type

Listing 3.13. Returning result and error from a function

Listing 3.14. Either type

Listing 3.15. Returning result or error from a function

Listing 3.16. Tagged union of shapes

Listing 3.17. Variant type

Listing 3.18. Union of shapes as variant

Listing 3.19. Naïve implementation

Listing 3.20. Processing with the visitor pattern

Listing 3.21. Variant visitor

Listing 3.22. Alternative processing with variant visitor

第 4 章类型安全

Chapter 4. Type safety

清单 4.1。不兼容组件的草图

清单 4.2。磅力秒和牛顿秒类型

清单 4.3。将 lbfs 转换为 Ns

清单 4.4。更新的组件

清单 4.5。构造函数抛出无效值

清单 4.6。构造函数强制无效值

清单 4.7。工厂在无效值上返回 undefined

清单 4.8。导致运行时错误的类型转换

清单 4.9。重温 Either 实现

清单 4.10。makeLeft 和 makeRight

清单 4.11。三角形或正方形

清单 4.12。isLeft() 和 getLeft()

清单 4.13。向上广播

清单 4.14。沮丧

清单 4.15。实现 IDocumentItem 的类型集合

清单 4.16。作为求和类型的类型集合

清单 4.17。未知类型的集合

清单 4.18。序列化一只猫

清单 4.19。反序列化一只猫

清单 4.20。序列化和跟踪类型

清单 4.21。使用跟踪类型反序列化

Listing 4.1. Sketch of incompatible components

Listing 4.2. Pound-force second and Newton-second types

Listing 4.3. Converting lbfs to Ns

Listing 4.4. Updated components

Listing 4.5. Constructor throwing on invalid value

Listing 4.6. Constructor coercing an invalid value

Listing 4.7. Factory returning undefined on invalid value

Listing 4.8. Type cast causing a run-time error

Listing 4.9. Revisiting Either implementation

Listing 4.10. makeLeft and makeRight

Listing 4.11. Triangle or Square

Listing 4.12. isLeft() and getLeft()

Listing 4.13. Upcast

Listing 4.14. Downcast

Listing 4.15. A collection of types implementing IDocumentItem

Listing 4.16. A collection of types as a sum type

Listing 4.17. A collection of unknown type

Listing 4.18. Serializing a cat

Listing 4.19. Deserializing a Cat

Listing 4.20. Serializing and tracking type

Listing 4.21. Deserializing with tracked type

第 5 章函数类型

Chapter 5. Function types

清单 5.1。洗车攻略

清单 5.2。重新审视洗车策略

清单 5.3。可插拔的欢迎程序

清单 5.4。你好世界.ts

清单 5.5。第一章.md

清单 5.6。状态机实现

清单 5.7。替代状态机实现

清单 5.8。渴望汽车生产

清单 5.9。懒车制作

清单 5.10。匿名汽车生产

清单 5.11。临时映射

清单 5.12。地图()

清单 5.13。使用地图()

清单 5.14。临时过滤

清单 5.15。筛选()

清单 5.16。使用过滤器()

清单 5.17。特设还原

清单 5.18。减少()

清单 5.19。使用减少()

Listing 5.1. Car-wash strategy

Listing 5.2. Car-wash strategy revisited

Listing 5.3. Pluggable Greeter

Listing 5.4. helloWorld.ts

Listing 5.5. Chapter1.md

Listing 5.6. State machine implementation

Listing 5.7. Alternative state machine implementation

Listing 5.8. Eager Car production

Listing 5.9. Lazy Car production

Listing 5.10. Anonymous Car production

Listing 5.11. Ad hoc mapping

Listing 5.12. map()

Listing 5.13. Using map()

Listing 5.14. Ad hoc filtering

Listing 5.15. filter()

Listing 5.16. Using filter()

Listing 5.17. Ad hoc reducing

Listing 5.18. reduce()

Listing 5.19. Using reduce()

第 6 章函数类型的高级应用

Chapter 6. Advanced applications of function types

清单 6.1。WidgetFactory装饰器

清单 6.2。功能部件工厂

清单 6.3。功能部件工厂装饰器

清单 6.4。装饰函数

清单 6.5。全球柜台

清单 6.6。面向对象计数器

清单 6.7。功能计数器

清单 6.8。可恢复计数器

清单 6.9。同步执行

清单 6.10。带回调的异步执行

清单 6.11。在事件循环中倒计时

清单 6.12。事件循环中的两个计数器

清单 6.13。带回调的计数器

清单 6.14。链接回调

清单 6.15。函数返回承诺

清单 6.16。链接承诺

清单 6.17。getUserName() 返回一个承诺

清单 6.18。拒绝承诺

清单 6.19。链接函数返回承诺

清单 6.20。不返回承诺的链接函数

清单 6.21。使用 Promise.all() 来排序执行

清单 6.22。使用 Promise.race() 来排序执行

清单 6.23。链接承诺审查

清单 6.24。使用异步/等待

Listing 6.1. WidgetFactory decorator

Listing 6.2. Functional widget factory

Listing 6.3. Functional widget factory decorator

Listing 6.4. Decorator function

Listing 6.5. Global counter

Listing 6.6. Object-oriented counter

Listing 6.7. Functional counter

Listing 6.8. Resumable counter

Listing 6.9. Synchronous execution

Listing 6.10. Asynchronous execution with callback

Listing 6.11. Counting down in an event loop

Listing 6.12. Two counters in an event loop

Listing 6.13. Counter with callback

Listing 6.14. Chaining callbacks

Listing 6.15. Functions returning promises

Listing 6.16. Chaining promises

Listing 6.17. getUserName() returning a promise

Listing 6.18. Rejecting a promise

Listing 6.19. Chaining functions returning promises

Listing 6.20. Chaining functions that don’t return promises

Listing 6.21. Using Promise.all() to sequence execution

Listing 6.22. Using Promise.race() to sequence execution

Listing 6.23. Chaining promises review

Listing 6.24. Using async/await

第 7 章子类型化

Chapter 7. Subtyping

清单 7.1。磅力秒和牛顿秒类型

清单 7.2。没有唯一符号的磅力秒和牛顿秒

清单 7.3。User 在结构上是 Named 的子类型

清单 7.4。模拟名义子类型

清单 7.5。反序列化任何

清单 7.6。用户的运行时类型检查

清单 7.7。使用未知的强类型

清单 7.8。TurnDirection 到角度的转换

清单 7.9。报错

清单 7.10。turnAngle() 使用 fail()

清单 7.11。turnAgain() 使用 fail() 并返回一个虚拟值

清单 7.12。turnAngle() 使用 fail() 并返回其结果

清单 7.13。三角形 | 正方形变三角形 | 方形 | 圆圈

清单 7.14。三角形 | 方形 | 圆为三角形 | 正方形

清单 7.15。等边三角形声明

清单 7.16。三角形 [] 作为形状 []

清单 7.17。LinkedList<Triangle> 作为 LinkedList<Shape>

清单 7.18。() => 三角形为 () => 形状

清单 7.19。() => 形状​​为 () => 三角形

清单 7.20。覆盖一个子类型作为返回类型的方法

清单 7.21。绘制和渲染函数

清单 7.22。使用 isRightAngled() 方法的形状和三角形

清单 7.23。更新了绘制和渲染函数

清单 7.24。尝试在 Triangle 的超类型上调用 isRightAngled()

Listing 7.1. Pound-force second and Newton-second types

Listing 7.2. Pound-force second and Newton-second without unique symbols

Listing 7.3. User is structurally a subtype of Named

Listing 7.4. Simulating nominal subtyping

Listing 7.5. Deserializing any

Listing 7.6. Run-time type checking for User

Listing 7.7. Stronger typing using unknown

Listing 7.8. TurnDirection to angle conversion

Listing 7.9. Error reporting

Listing 7.10. turnAngle() using fail()

Listing 7.11. turnAgain() using fail() and returning a dummy value

Listing 7.12. turnAngle() using fail() and returning its result

Listing 7.13. Triangle | Square as Triangle | Square | Circle

Listing 7.14. Triangle | Square | Circle as Triangle | Square

Listing 7.15. EquilateralTriangle declaration

Listing 7.16. Triangle[] as Shape[]

Listing 7.17. LinkedList<Triangle> as LinkedList<Shape>

Listing 7.18. () => Triangle as () => Shape

Listing 7.19. () => Shape as () => Triangle

Listing 7.20. Overriding a method with a subtype as return type

Listing 7.21. Draw and render functions

Listing 7.22. Shape and Triangle with isRightAngled() method

Listing 7.23. Updated draw and render functions

Listing 7.24. Attempting to call isRightAngled() on a supertype of Triangle

第 8 章面向对象编程的要素

Chapter 8. Elements of object-oriented programming

清单 8.1。抽象记录器

清单 8.2。记录器接口

清单 8.3。扩展记录器接口

清单 8.4。组合接口

清单 8.5。不良继承

清单 8.6。表达层次

清单 8.7。继承与组合

清单 8.8。询问首席执行官

清单 8.9。几何库

清单 8.10。圆形适配器

清单 8.11。狩猎行为

清单 8.12。用另一个实例的成员扩展一个实例

清单 8.13。混合行为

清单 8.14。OOP 访问者

清单 8.15。有变体的访客

清单 8.16。函数表达式

Listing 8.1. Abstract logger

Listing 8.2. Logger interface

Listing 8.3. Extended logger interface

Listing 8.4. Combining interfaces

Listing 8.5. Bad inheritance

Listing 8.6. Expression hierarchy

Listing 8.7. Inheritance and composition

Listing 8.8. Ask the CEO

Listing 8.9. Geometry library

Listing 8.10. CircleAdapter

Listing 8.11. Hunting behavior

Listing 8.12. Extending an instance with the members of another one

Listing 8.13. Mixing in behavior

Listing 8.14. Visitor with OOP

Listing 8.15. Visitor with Variant

Listing 8.16. Functional expressions

第 9 章通用数据结构

Chapter 9. Generic data structures

清单 9.1。获取数字()

清单 9.2。默认变换()

清单 9.3。组装小部件()

清单 9.4。默认 pluck()

清单 9.5。doNothing() 和 pluckAll()

清单 9.6。天真的身份

清单 9.7。任何不安全的使用

清单 9.8。通用标识

清单 9.9。类型安全

清单 9.10。可选类型

清单 9.11。数字的二叉树

清单 9.12。字符串链表

清单 9.13。通用二叉树

清单 9.14。通用链表

清单 9.15。按顺序打印

清单 9.16。printInOrder() 示例

清单 9.17。打印链表

清单 9.18。printLinkedList() 例子

清单 9.19。迭代器结果

清单 9.20。迭代器接口

清单 9.21。二叉树迭代器

清单 9.22。链表迭代器

清单 9.23。print() 使用迭代器

清单 9.24。contains() 使用迭代器

清单 9.25。可迭代接口

清单 9.26。可迭代链表

清单 9.27。带有 Iterator 参数的 print() 和 contains()

清单 9.28。带有 Iterable 参数的 print() 和 contains()

清单 9.29。二叉树迭代器

清单 9.30。使用生成器的二叉树迭代器

清单 9.31。链表迭代器

清单 9.32。使用生成器的链表迭代器

清单 9.33。使用生成器的可迭代链表

清单 9.34。无限的随机数流

清单 9.35。使用流中的值

清单 9.36。正方形()

清单 9.37。拿()

清单 9.38。管道

Listing 9.1. getNumbers()

Listing 9.2. Default transform()

Listing 9.3. assembleWidgets()

Listing 9.4. Default pluck()

Listing 9.5. doNothing() and pluckAll()

Listing 9.6. Naïve identity

Listing 9.7. Unsafe use of any

Listing 9.8. Generic identity

Listing 9.9. Type safety

Listing 9.10. Optional type

Listing 9.11. Binary tree of numbers

Listing 9.12. Linked list of strings

Listing 9.13. Generic binary tree

Listing 9.14. Generic linked list

Listing 9.15. Print in order

Listing 9.16. printInOrder() example

Listing 9.17. Print linked list

Listing 9.18. printLinkedList() example

Listing 9.19. Iterator result

Listing 9.20. Iterator interface

Listing 9.21. Binary tree iterator

Listing 9.22. Linked list iterator

Listing 9.23. print() using iterator

Listing 9.24. contains() using iterator

Listing 9.25. Iterable interface

Listing 9.26. Iterable linked list

Listing 9.27. print() and contains() with Iterator argument

Listing 9.28. print() and contains() with Iterable argument

Listing 9.29. Binary tree iterator

Listing 9.30. Binary tree iterator using generator

Listing 9.31. Linked list iterator

Listing 9.32. Linked list iterator using generator

Listing 9.33. Iterable linked list using generator

Listing 9.34. Inifinite stream of random numbers

Listing 9.35. Consuming values from the stream

Listing 9.36. square()

Listing 9.37. take()

Listing 9.38. Pipeline

第 10 章通用算法和迭代器

Chapter 10. Generic algorithms and iterators

清单 10.1。地图()

清单 10.2。带迭代器的 map()

清单 10.3。筛选()

清单 10.4。filter() 与迭代器

清单 10.5。减少()

清单 10.6。reduce() 与迭代器

清单 10.7。过滤器()/减少()管道

清单 10.8。过滤/减少管道

清单 10.9。流利的可迭代

清单 10.10。流畅的过滤器/减少管道

清单 10.11。更流畅的迭代

清单 10.12。更流畅的过滤器/减少管道

清单 10.13。渲染草图

清单 10.14。带约束的 renderAll

清单 10.15。比较接口

清单 10.16。最大()算法

清单 10.17。带有 compare() 参数的 max() 算法

清单 10.18。反向()与堆栈

清单 10.19。数组的反向()

清单 10.20。IReadable<T> 和 IIncrementable<T>

清单 10.21。IInputIterator<T>

清单 10.22。链表实现

清单 10.23。链表输入迭代器

清单 10.24。链表上的一对迭代器

清单 10.25。IWritable<T> 和 IOutputIterator<T>

清单 10.26。控制台输出迭代器

清单 10.27。带有输入和输出迭代器的 map()

清单 10.28。find() 可迭代

清单 10.29。IForwardIterator<T>

清单 10.30。LinkedListIterator<T> 实现 IForwardIterator<T>

清单 10.31。带前向迭代器的 find()

清单 10.32。在链表中用 0 替换 42

清单 10.33。IBidirectionalIterator<T> 和 ArrayIterator<T>

清单 10.34。reverse() 与双向迭代器

清单 10.35。反转数字数组

清单 10.36。IRandomAccessIterator<T>

清单 10.37。ArrayIterator<T> 实现随机访问迭代器

清单 10.38。元素在

清单 10.39。带有输入和随机访问迭代器的 elementAt()

清单 10.40。自适应元素At()

Listing 10.1. map()

Listing 10.2. map() with iterator

Listing 10.3. filter()

Listing 10.4. filter() with iterator

Listing 10.5. reduce()

Listing 10.6. reduce() with iterator

Listing 10.7. filter()/reduce() pipeline

Listing 10.8. filter/reduce pipeline

Listing 10.9. Fluent iterable

Listing 10.10. Fluent filter/reduce pipeline

Listing 10.11. Better fluent iterable

Listing 10.12. Better fluent filter/reduce pipeline

Listing 10.13. renderAll sketch

Listing 10.14. renderAll with constraint

Listing 10.15. IComparable interface

Listing 10.16. max() algorithm

Listing 10.17. max() algorithm with compare() argument

Listing 10.18. reverse() with stack

Listing 10.19. reverse() for array

Listing 10.20. IReadable<T> and IIncrementable<T>

Listing 10.21. IInputIterator<T>

Listing 10.22. Linked list implementation

Listing 10.23. Linked list input iterator

Listing 10.24. Pair of iterators over linked list

Listing 10.25. IWritable<T> and IOutputIterator<T>

Listing 10.26. Console output iterator

Listing 10.27. map() with input and output iterators

Listing 10.28. find() with iterable

Listing 10.29. IForwardIterator<T>

Listing 10.30. LinkedListIterator<T> implementing IForwardIterator<T>

Listing 10.31. find() with forward iterator

Listing 10.32. Replacing 42 with 0 in a linked list

Listing 10.33. IBidirectionalIterator<T> and ArrayIterator<T>

Listing 10.34. reverse() with bidirectional iterator

Listing 10.35. Reversing an array of numbers

Listing 10.36. IRandomAccessIterator<T>

Listing 10.37. ArrayIterator<T> implementing a random-access iterator

Listing 10.38. Element at

Listing 10.39. elementAt() with input and random-access iterators

Listing 10.40. Adaptive elementAt()

第 11 章高等类型及更高类型

Chapter 11. Higher kinded types and beyond

清单 11.1。通用地图()

清单 11.2。可选类型

清单 11.3。可选地图()

清单 11.4。求和类型 map()

清单 11.5。箱型

清单 11.6。盒图()

清单 11.7。square() 和 stringify()

清单 11.8。readNumber() 返回类型

清单 11.9。处理号码

清单 11.10。使用 map() 处理

清单 11.11。使用 lambda 处理

清单 11.12。解包 square() 的值

清单 11.13。解压 stringify() 的值

清单 11.14。使用地图()

清单 11.15。Functor接口示意图

清单 11.16。实现接口的盒子

清单 11.17。函子接口

清单 11.18。函数映射()

清单 11.19。在函数上应用 map()

清单 11.20。返回结果或错误的函数

清单 11.21。任一种类型

清单 11.22。处理并显式检查错误

清单 11.23。地图()

清单 11.24。不兼容的类型

清单 11.25。绑定()

清单 11.26。无分支 readCatFromFile()

清单 11.27。盒子上的地图()

清单 11.28。盒子上的绑定()

清单 11.29。盒单子

清单 11.30。可选的单子

清单 11.31。链接承诺

清单 11.32。除数

清单 11.33。所有除数

清单 11.34。所有字谜

清单 11.35。列表绑定()

Listing 11.1. Generic map()

Listing 11.2. Optional type

Listing 11.3. Optional map()

Listing 11.4. Sum type map()

Listing 11.5. Box type

Listing 11.6. Box map()

Listing 11.7. square() and stringify()

Listing 11.8. readNumber() return type

Listing 11.9. Processing a number

Listing 11.10. Processing with map()

Listing 11.11. Processing with lambda

Listing 11.12. Unpacking values for square()

Listing 11.13. Unpacking values for stringify()

Listing 11.14. Using map()

Listing 11.15. Sketch of Functor interface

Listing 11.16. Box implementing the interface

Listing 11.17. Functor interface

Listing 11.18. Function map()

Listing 11.19. Applying map() over a function

Listing 11.20. Functions returning result or error

Listing 11.21. Either type

Listing 11.22. Processing and explicitly checking for errors

Listing 11.23. Either map()

Listing 11.24. Incompatible types

Listing 11.25. Either bind()

Listing 11.26. Branchless readCatFromFile()

Listing 11.27. map() on Box

Listing 11.28. bind() on Box

Listing 11.29. Box monad

Listing 11.30. Optional monad

Listing 11.31. Chaining promises

Listing 11.32. Divisors

Listing 11.33. All divisors

Listing 11.34. All anagrams

Listing 11.35. List bind()